diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/CliHelpers.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/CliHelpers.cs new file mode 100644 index 0000000..3cd1013 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/CliHelpers.cs @@ -0,0 +1,90 @@ + +using PatreonPatcher.Core; +using PatreonPatcher.Core.Helpers; +using PatreonPatcher.Core.Logging; +using System.CommandLine; +using System.CommandLine.IO; +using System.Runtime.InteropServices; + +internal static class CliHelpers +{ + public static bool IsValidUnityDirectory(string path) + { + if (File.Exists(path)) + { + path = Path.GetDirectoryName(path) ?? string.Empty; + } + + return File.Exists(Path.Combine(path, Constants.UnityPlayerAssembly)); + } + + public static async Task WaitUserSelectGameExecutable(CancellationToken ct = default) + { + // check if OS is windows + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Log.Error("This feature is only available on Windows."); + throw new PlatformNotSupportedException("This feature is only available on Windows."); + } + Task task = Task.Run(() => + { + string? gameExecutable; + do + { + gameExecutable = WindowsUtils.ShowOpenFileDialog($"AntroHeat.exe\0*.exe\0"); + if (gameExecutable == null || !IsValidUnityDirectory(gameExecutable)) + { + if (!WindowsUtils.ShowOkCancelMessageBox("Please select the game executable or press cancel.", "Invalid game executable")) + { + return null; + } + gameExecutable = null; + } + } while (gameExecutable == null); + return gameExecutable; + }, ct); + return await task; + } + + public static Task WaitForUserInputAsync() + { + return Console.IsOutputRedirected || Console.IsErrorRedirected + ? Task.CompletedTask + : Task.Run(() => + { + Console.WriteLine("Press any key to continue..."); + _ = Console.ReadKey(true); + }); + } + + public static void WriteErrorMessage(IConsole console) + { + console.WriteLine( +@" + _____ _ _ _ _ + / ____| | | | | (_) | | + | (___ ___ _ __ ___ ___| |_| |__ _ _ __ __ _ __ _____ _ __ | |_ __ ___ __ ___ _ __ __ _ + \___ \ / _ \| '_ ` _ \ / _ \ __| '_ \| | '_ \ / _` | \ \ /\ / / _ \ '_ \| __| \ \ /\ / / '__/ _ \| '_ \ / _` | + ____) | (_) | | | | | | __/ |_| | | | | | | | (_| | \ V V / __/ | | | |_ \ V V /| | | (_) | | | | (_| | + |_____/ \___/|_| |_| |_|\___|\__|_| |_|_|_| |_|\__, | \_/\_/ \___|_| |_|\__| \_/\_/ |_| \___/|_| |_|\__, | + __/ | __/ | + |___/ |___/ +", ConsoleColor.Red); + console.WriteLine("Sorry for the inconvenience, but the game could not be patched."); + console.WriteLine("If you believe this is an error, please open an issue at:"); + console.WriteLine("https://github.com/OpenYiffGames/HeatGame/issues", ConsoleColor.Blue); + } + + public static void WriteDoneMessage(IConsole console) + { + console.WriteLine( +@" + ____ _ +| _ \ ___ _ __ ___| | +| | | |/ _ \| '_ \ / _ \ | +| |_| | (_) | | | | __/_| +|____/ \___/|_| |_|\___(_) +", ConsoleColor.Green); + console.WriteLine("Patching successful!"); + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/Commands/PatchCommand.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/Commands/PatchCommand.cs new file mode 100644 index 0000000..4718674 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/Commands/PatchCommand.cs @@ -0,0 +1,113 @@ +using PatreonPatcher.Core; +using PatreonPatcher.Core.Logging; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.Diagnostics.CodeAnalysis; + +namespace PatreonPatcher.Cli.Commands; + +[RequiresDynamicCode("Calls PatreonPatcher.src.Patcher.PatchAsync()")] +internal class PatchCommand : RootCommand, ICommandHandler +{ + public const string CliModeOptionName = "--cli"; + + private readonly Option cliModeOption = new( + name: CliModeOptionName, + description: "Used internally on windows to signal if not running in CLI mode (no GUI)", + getDefaultValue: () => true) + { + IsRequired = false, + IsHidden = true + }; + + private readonly Argument gameExecutableArgument = new() + { + Name = "gameExecutable", + Description = "The path to the game executable.", + }; + + public PatchCommand(PlatformID platformID) : base("") + { + AddArgument(gameExecutableArgument); + AddOption(cliModeOption); + AddValidator(CommandValidator); + Handler = this; + + ArgumentArity arity = platformID switch + { + PlatformID.Win32NT => ArgumentArity.ZeroOrOne, + PlatformID.Unix => ArgumentArity.ExactlyOne, + _ => ArgumentArity.ExactlyOne + }; + string workingDirectory = Environment.CurrentDirectory; + gameExecutableArgument.Arity = arity; + gameExecutableArgument.Completions.Add(new DirectoryFilesCompletionSource(new DirectoryInfo(workingDirectory)) + { + MatchFilterPredicate = (file) => + CliHelpers.IsValidUnityDirectory(file.FullName), + FileFilter = platformID switch + { + PlatformID.Win32NT => "*.exe", + PlatformID.Unix => "*", + _ => "*" + } + }); + } + + public int Invoke(InvocationContext context) + { + return SyncWithAsync(context); + } + + public async Task InvokeAsync(InvocationContext context) + { + FileInfo gameExecutable = context.ParseResult.GetValueForArgument(gameExecutableArgument) + ?? throw new InvalidOperationException("Game executable is null."); + + Log.Debug("running [PatchCommand] Game executable: " + gameExecutable.FullName); + + IConsole console = context.Console; + Patcher patcher = Patcher.Create(gameExecutable.FullName); + bool ok = await patcher.PatchAsync(); + if (ok) + { + Log.Info("Patch completed successfully."); + CliHelpers.WriteDoneMessage(console); + } + else + { + Log.Error("Patch failed."); + CliHelpers.WriteErrorMessage(console); + } + bool cliMode = context.ParseResult.GetValueForOption(cliModeOption); + if (!cliMode) + { + await CliHelpers.WaitForUserInputAsync(); + } + return ok ? 0 : 1; + } + + private void CommandValidator(CommandResult command) + { + FileInfo? gameExecutable = command.GetValueForArgument(gameExecutableArgument); + if (gameExecutable is null) + { + return; + } + if (!gameExecutable.Exists) + { + command.ErrorMessage = $"The file '{gameExecutable.FullName}' does not exist."; + } + if (!CliHelpers.IsValidUnityDirectory(gameExecutable.DirectoryName ?? gameExecutable.FullName)) + { + command.ErrorMessage = $"The file '{gameExecutable.FullName}' is not a valid Unity executable."; + } + } + + private int SyncWithAsync(InvocationContext context) + { + Task task = InvokeAsync(context); + return task.GetAwaiter().GetResult(); + } +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/ConsoleExtensions.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/ConsoleExtensions.cs new file mode 100644 index 0000000..1b09737 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/ConsoleExtensions.cs @@ -0,0 +1,78 @@ + +using System.CommandLine; +using System.CommandLine.IO; +using System.Runtime.InteropServices; + +internal static class ConsoleExtensions +{ + public static void Write(this IConsole console, string value, ConsoleColor color) + { + if (NoColor || console.IsOutputRedirected || !PlataformSupportsAnsi()) + { + console.Write(value); + return; + } + string ansiColorCode = GetAnsiColorCode(color); + console.Write(ansiColorCode); + console.Write(value); + console.Write(AnsiReset); + } + + public static void WriteLine(this IConsole console, string value, ConsoleColor color) + { + if (NoColor || console.IsOutputRedirected || !PlataformSupportsAnsi()) + { + console.WriteLine(value); + return; + } + string ansiColorCode = GetAnsiColorCode(color); + console.Write(ansiColorCode); + console.Write(value); + console.WriteLine(AnsiReset); + } + + private const string AnsiReset = "\u001b[0m"; + + public static bool NoColor => Environment.GetEnvironmentVariable("NO_COLOR") != null; + + private static bool PlataformSupportsAnsi() + { + var currentPlatform = Environment.OSVersion.Platform; + if (currentPlatform == PlatformID.Win32NT) + { + return IsWindows10OrGreater(); + } + return currentPlatform is PlatformID.Unix or PlatformID.MacOSX; + } + + private static bool IsWindows10OrGreater() + { + _ = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + return Environment.OSVersion.Version.Major >= 10; + } + + private static string GetAnsiColorCode(ConsoleColor color) + { + const string commandBase = $"\u001b["; + return color switch + { + ConsoleColor.Black => $"{commandBase}30m", + ConsoleColor.DarkRed => $"{commandBase}31m", + ConsoleColor.DarkGreen => $"{commandBase}32m", + ConsoleColor.DarkYellow => $"{commandBase}33m", + ConsoleColor.DarkBlue => $"{commandBase}34m", + ConsoleColor.DarkMagenta => $"{commandBase}35m", + ConsoleColor.DarkCyan => $"{commandBase}36m", + ConsoleColor.Gray => $"{commandBase}37m", + ConsoleColor.DarkGray => $"{commandBase}90m", + ConsoleColor.Red => $"{commandBase}91m", + ConsoleColor.Green => $"{commandBase}92m", + ConsoleColor.Yellow => $"{commandBase}93m", + ConsoleColor.Blue => $"{commandBase}94m", + ConsoleColor.Magenta => $"{commandBase}95m", + ConsoleColor.Cyan => $"{commandBase}96m", + ConsoleColor.White => $"{commandBase}97m", + _ => $"{commandBase}0m" // reset + }; + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/DirectoryFilesCompletionSource.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/DirectoryFilesCompletionSource.cs new file mode 100644 index 0000000..ba60ae3 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/DirectoryFilesCompletionSource.cs @@ -0,0 +1,42 @@ +using System.CommandLine.Completions; + +namespace PatreonPatcher.Cli +{ + internal class DirectoryFilesCompletionSource : ICompletionSource + { + private readonly DirectoryInfo _directoryInfo; + public string? FileFilter { get; set; } = null; + public MatchType MatchType { get; set; } = MatchType.Win32; + public MatchCasing MatchCasing { get; set; } = MatchCasing.CaseInsensitive; + public Func MatchFilterPredicate { get; set; } = _ => true; + + private string AllFilesFilter => MatchType switch + { + MatchType.Win32 => "*.*", + MatchType.Simple => "*", + _ => "*.*" + }; + + public DirectoryFilesCompletionSource(DirectoryInfo directoryInfo) + { + _directoryInfo = directoryInfo; + } + + public IEnumerable GetCompletions(CompletionContext context) + { + string possibleFileName = context.WordToComplete; + + IEnumerable matchingFiles = _directoryInfo.GetFiles(FileFilter ?? AllFilesFilter, new EnumerationOptions() + { + MatchType = MatchType, + MatchCasing = MatchCasing, + MaxRecursionDepth = 1, + }) + .Where(file => file.Name.StartsWith(possibleFileName, StringComparison.OrdinalIgnoreCase)) + .Where(MatchFilterPredicate) + .Select(file => new CompletionItem(file.Name)); + + return matchingFiles; + } + } +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/Program.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/Program.cs new file mode 100644 index 0000000..e4ee19a --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/Program.cs @@ -0,0 +1,97 @@ +using PatreonPatcher.Cli; +using PatreonPatcher.Cli.Commands; +using PatreonPatcher.Core; +using PatreonPatcher.Core.Helpers; +using PatreonPatcher.Core.Logging; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.IO; +using System.CommandLine.Parsing; +using System.Diagnostics.CodeAnalysis; + +[RequiresDynamicCode("Calls PatreonPatcher.src.Patcher.PatchAsync()")] +internal static class Program +{ + private static async Task Main(string[] args) + { + SystemConsole console = new(); + string logFile = Path.Combine( + Utils.GetLocalStorageDirectory(), + Constants.DefaultLogFileName + ); + SystemConsoleLogSink consoleLogSink = new(console) + { + LogLevel = LogLevel.Info + }; + using Logger logger = new Logger() + .AddFileLogging(logFile, maxSizeMB: 10, logLevel: LogLevel.Debug) + .AddOutputSink(consoleLogSink); + + Log.Logger = logger; + + PlatformID platformID = Environment.OSVersion.Platform; + + PatchCommand rootCommand = SetupRootCommand(platformID); + + CommandLineBuilder cmdBuilder = new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseHelp() + .UseExceptionHandler(async (e, ctx) => + { + Log.Debug($"Exception caught in the exception handler: [{e}]"); + bool cliMode = args.Length > 0; + await ExceptionHandler(ctx.Console, e, waitUserInput: !cliMode); + }); + + cmdBuilder.AddLoggingMidleware(consoleLogSink); + + if (platformID == PlatformID.Win32NT) + { + cmdBuilder.AddWindowsFileDialogMiddleware(); + } + + Parser parser = cmdBuilder.Build(); + return await parser.InvokeAsync(args, console); + } + + private static PatchCommand SetupRootCommand(PlatformID platformID) + { + PatchCommand rootCommand = new(platformID); + return rootCommand; + } + + private static async Task ExceptionHandler(IConsole console, Exception e, bool waitUserInput = false) + { + Log.Error("An error occurred while patching: " + e.Message); + console.WriteLine("=================| STACK TRACE |================="); + console.WriteLine(e.StackTrace ?? ""); + console.WriteLine("=================================================\n\n"); + CliHelpers.WriteErrorMessage(console); + if (waitUserInput) + { + await CliHelpers.WaitForUserInputAsync(); + } + } + + private static void AddLoggingMidleware(this CommandLineBuilder builder, SystemConsoleLogSink console) + { + Option verboseOption = new("--verbose", "Enable verbose logging") + { + ArgumentHelpName = "verbose" + }; + builder.Command.AddGlobalOption(verboseOption); + + _ = builder.AddMiddleware((context) => + { + bool verbose = context.ParseResult.GetValueForOption(verboseOption); + if (verbose) + { + console.LogLevel = LogLevel.Debug; + Log.Debug("Verbose logging enabled."); + } + Symbol symbol = context.ParseResult.CommandResult.Symbol; + Log.Debug($"[CLI pipeline] command result symbol: {symbol.Name}"); + Log.Debug("[CLI pipeline] command result tokens: {0}", context.ParseResult.CommandResult.Tokens); + }); + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/SystemConsoleLogSink.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/SystemConsoleLogSink.cs new file mode 100644 index 0000000..075ea16 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/SystemConsoleLogSink.cs @@ -0,0 +1,34 @@ +using PatreonPatcher.Core.Logging; +using System.CommandLine; + +namespace PatreonPatcher.Cli; + +internal class SystemConsoleLogSink : ILoggerSink +{ + private readonly IConsole _console; + + public LogLevel LogLevel { get; set; } = LogLevel.Debug; + + public SystemConsoleLogSink(IConsole console) + { + _console = console; + } + + private static readonly Dictionary Colors = new() + { + [LogLevel.Debug] = ConsoleColor.Blue, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warning] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red + }; + + public void Write(LogLevel level, string message) + { + ConsoleColor color = Colors[level]; + _console.WriteLine($"[{level}] {message}", color); + } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Cli/WindowsFileDialogMiddleware.cs b/Tools/AuthPatcher/PatreonPatcher/Cli/WindowsFileDialogMiddleware.cs new file mode 100644 index 0000000..d5f015f --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Cli/WindowsFileDialogMiddleware.cs @@ -0,0 +1,38 @@ +using PatreonPatcher.Cli.Commands; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Parsing; + +internal static class WindowsFileDialogMiddleware +{ + public static void AddWindowsFileDialogMiddleware(this CommandLineBuilder builder) + { + _ = builder.AddMiddleware(new System.CommandLine.Invocation.InvocationMiddleware(async (context, next) => + { + ParseResult result = context.ParseResult; + if (result.Errors.Count > 0) + { + await next(context); + return; + } + + IReadOnlyList tokens = context.ParseResult.RootCommandResult.Tokens; + + if (tokens.Count == 0) + { + CancellationToken ct = context.GetCancellationToken(); + string? gameExecutable = await CliHelpers.WaitUserSelectGameExecutable(ct); + if (gameExecutable is null) + { + context.ExitCode = 1; + return; + } + Argument gameExeArg = context.ParseResult.RootCommandResult.Command.Arguments[0]; + gameExeArg.Arity = ArgumentArity.ExactlyOne; + result = gameExeArg.Parse([gameExecutable, PatchCommand.CliModeOptionName, "false"]); + context.ParseResult = result; + } + await next(context); + })); + } +} diff --git a/Tools/AuthPatcher/PatreonPatcher/src/CLRTypes.cs b/Tools/AuthPatcher/PatreonPatcher/Core/CLRTypes.cs similarity index 81% rename from Tools/AuthPatcher/PatreonPatcher/src/CLRTypes.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/CLRTypes.cs index c54b99f..d964444 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/CLRTypes.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/CLRTypes.cs @@ -1,9 +1,9 @@ using System.Runtime.InteropServices; -namespace PatreonPatcher; +namespace PatreonPatcher.Core; [StructLayout(LayoutKind.Explicit)] -struct IMAGE_COR_ILMETHOD +internal struct IMAGE_COR_ILMETHOD { [FieldOffset(0)] public byte Tiny_Flags_CodeSize; diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Constants.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Constants.cs similarity index 93% rename from Tools/AuthPatcher/PatreonPatcher/src/Constants.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Constants.cs index 09996a9..25c0e91 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Constants.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Constants.cs @@ -1,4 +1,4 @@ -namespace PatreonPatcher; +namespace PatreonPatcher.Core; internal class Constants { @@ -23,6 +23,8 @@ public static class Patterns public const string BypassAuthFunction = "02 28 ?? ?? ?? 06 02 28 ?? ?? ?? 06 2A"; } + public const string DefaultLogFileName = "Log.txt"; + public const string UnityEngineAssembly = "UnityEngine.dll"; public const string UnityPlayerAssembly = "UnityPlayer.dll"; diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.Pattern.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.Pattern.cs similarity index 67% rename from Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.Pattern.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.Pattern.cs index 6a35e25..31b4859 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.Pattern.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.Pattern.cs @@ -3,9 +3,9 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; -namespace PatreonPatcher.Helpers; +namespace PatreonPatcher.Core.Helpers; -static partial class Utils +internal static partial class Utils { public static class Pattern { @@ -20,15 +20,16 @@ public static bool MethodMatchesPattern(Stream assemblySource, MethodDef method, uint codeSize; lock (assemblySource) { - header = ReadMethodHeader(method, assemblySource, out var isBigHeader) + header = ReadMethodHeader(method, assemblySource, out bool isBigHeader) ?? throw new Exception("Failed to read method header"); codeSize = isBigHeader ? header.Fat_CodeSize : header.Tiny_Flags_CodeSize; } - var pool = ArrayPool.Shared.Rent((int)codeSize); + byte[] pool = ArrayPool.Shared.Rent((int)codeSize); try { - byte[]? methodCilBody = GetCilBodyBytes(assemblySource, method, pool) ?? throw new Exception("Failed to read method body"); - var offset = patternScanner.Find(methodCilBody); + byte[]? methodCilBody = GetCilBodyBytes(assemblySource, method, pool) + ?? throw new Exception("Failed to read method body"); + int offset = patternScanner.Find(methodCilBody); return offset >= 0; } finally @@ -39,18 +40,18 @@ public static bool MethodMatchesPattern(Stream assemblySource, MethodDef method, public static bool MethodMatchesPattern(PEReader reader, MethodDefinition methodDef, PatternScanner patternScanner) { - var rva = methodDef.RelativeVirtualAddress; + int rva = methodDef.RelativeVirtualAddress; if (rva == 0) { return false; } - var ilMethod = reader.GetMethodBody(rva); + MethodBodyBlock ilMethod = reader.GetMethodBody(rva); if (ilMethod.Size == 0) { return false; } - var ilReader = ilMethod.GetILReader(); - var buffer = ArrayPool.Shared.Rent(ilMethod.Size); + BlobReader ilReader = ilMethod.GetILReader(); + byte[] buffer = ArrayPool.Shared.Rent(ilMethod.Size); try { ilReader.ReadBytes(ilReader.RemainingBytes, buffer, 0); @@ -67,33 +68,29 @@ public static bool MethodMatchesPattern(PEReader reader, MethodDefinition method string pattern, bool throwOnMultipleMatches = true) { - var scanResult = FindMethodsRVAMatchingPattern(assemblyStream, pattern); - if (scanResult.Length == 0) - { - return null; - } - if (scanResult.Length > 1 && throwOnMultipleMatches) - { - throw new Exception($"Multiple methods found for pattern {pattern}"); - } - return scanResult[0]; + int[] scanResult = FindMethodsRVAMatchingPattern(assemblyStream, pattern); + return scanResult.Length == 0 + ? null + : throwOnMultipleMatches + ? scanResult.Single() + : scanResult.FirstOrDefault(); } public static int[] FindMethodsRVAMatchingPattern( Stream assemblyStream, string pattern) { - var scanner = new PatternScanner(pattern); - var reader = new PEReader(assemblyStream); + PatternScanner scanner = new(pattern); + PEReader reader = new(assemblyStream); if (!reader.HasMetadata) { throw new Exception("No metadata found in assembly"); } - var metadata = reader.GetMetadataReader(); - var methods = metadata.MethodDefinitions; + MetadataReader metadata = reader.GetMetadataReader(); + MethodDefinitionHandleCollection methods = metadata.MethodDefinitions; - var scanResult = methods.AsParallel() + ParallelQuery scanResult = methods.AsParallel() .WithDegreeOfParallelism(4) .Select(metadata.GetMethodDefinition) .Where(methodDef => MethodMatchesPattern(reader, methodDef, scanner)) @@ -104,15 +101,15 @@ public static int[] FindMethodsRVAMatchingPattern( public static MethodDef? FindTypeMethodMatchingPattern(Stream source, TypeDef typeDef, string pattern) { - var methods = typeDef.Methods; - var scanner = new PatternScanner(pattern); + IList methods = typeDef.Methods; + PatternScanner scanner = new(pattern); return methods.FirstOrDefault(method => MethodMatchesPattern(source, method, scanner)); } public static MethodDef[] FindTypeMethodsMatchingPattern(Stream source, TypeDef typeDef, string pattern) { - var methods = typeDef.Methods; - var scanner = new PatternScanner(pattern); + IList methods = typeDef.Methods; + PatternScanner scanner = new(pattern); return methods.Where(method => MethodMatchesPattern(source, method, scanner)) .ToArray(); } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.cs similarity index 70% rename from Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.cs index 7a3fc08..d252a0a 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/Utils.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/Utils.cs @@ -7,9 +7,9 @@ using System.Runtime.InteropServices; using System.Text; -namespace PatreonPatcher.Helpers; +namespace PatreonPatcher.Core.Helpers; -static partial class Utils +internal static partial class Utils { public static byte[]? GetCilBodyBytes(Stream assemblySource, MethodDef method, byte[]? buffer = null) { @@ -19,14 +19,14 @@ static partial class Utils } lock (assemblySource) { - var header = ReadMethodHeader(method, assemblySource, out var isBigHeader); + IMAGE_COR_ILMETHOD? header = ReadMethodHeader(method, assemblySource, out bool isBigHeader); if (header == null) { return null; } uint codeSize = isBigHeader ? header.Value.Fat_CodeSize : header.Value.Tiny_Flags_CodeSize; byte[] methodCilBody = buffer ?? new byte[codeSize]; - assemblySource.Read(methodCilBody.AsSpan(0, (int)codeSize)); + _ = assemblySource.Read(methodCilBody.AsSpan(0, (int)codeSize)); return methodCilBody; } } @@ -39,7 +39,7 @@ static partial class Utils { return null; } - stream.Seek(bodyOffset, SeekOrigin.Begin); + _ = stream.Seek(bodyOffset, SeekOrigin.Begin); int ImageCorILMethodSize = Marshal.SizeOf(); Span buffer = stackalloc byte[ImageCorILMethodSize]; @@ -47,12 +47,12 @@ static partial class Utils { buffer = buffer[..1]; } - stream.Read(buffer); + _ = stream.Read(buffer); IntPtr ptr = Marshal.AllocHGlobal(ImageCorILMethodSize); try { - var unmanagedBufferSpan = MemoryMarshal.CreateSpan(ref Unsafe.AddByteOffset(ref Unsafe.NullRef(), ptr), ImageCorILMethodSize); + Span unmanagedBufferSpan = MemoryMarshal.CreateSpan(ref Unsafe.AddByteOffset(ref Unsafe.NullRef(), ptr), ImageCorILMethodSize); buffer.CopyTo(unmanagedBufferSpan); return Marshal.PtrToStructure(ptr); } @@ -64,25 +64,26 @@ static partial class Utils public static uint SwapEndianness(uint value) { - return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 | (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24; + return ((value & 0x000000FFU) << 24) | ((value & 0x0000FF00U) << 8) | ((value & 0x00FF0000U) >> 8) | ((value & 0xFF000000U) >> 24); } public static long RVA2FileOffset(MethodDef methodDef) { - if (methodDef.Module is not ModuleDefMD module) - { - return -1; - } - return (long)module.Metadata.PEImage.ToFileOffset(methodDef.RVA); + return methodDef.Module is not ModuleDefMD module ? -1 : (long)module.Metadata.PEImage.ToFileOffset(methodDef.RVA); } - public static string GetLocalStorageDirectory() + public static string GetLocalStorageDirectory(bool ensureExists = true) { - var assembly = Assembly.GetExecutingAssembly(); - var assemblyGuid = (assembly.GetCustomAttribute()?.Value) ?? assembly.GetName().FullName; - return Path.Combine( + Assembly assembly = Assembly.GetExecutingAssembly(); + string assemblyGuid = (assembly.GetCustomAttribute()?.Value) ?? assembly.GetName().FullName; + string path = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), assemblyGuid); + if (ensureExists && !Directory.Exists(path)) + { + _ = Directory.CreateDirectory(path); + } + return path; } public static AssemblyName GetAssemblyName(Stream peFileS) @@ -91,7 +92,7 @@ public static AssemblyName GetAssemblyName(Stream peFileS) try { using PEReader reader = new(peFileS, PEStreamOptions.LeaveOpen); - var metadata = reader.GetMetadataReader(); + MetadataReader metadata = reader.GetMetadataReader(); return metadata.GetAssemblyDefinition().GetAssemblyName(); } catch @@ -106,11 +107,11 @@ public static AssemblyName GetAssemblyName(Stream peFileS) public static async Task AsStreamAsync(this AssemblyDef assemblyDef, CancellationToken token = default) { - var ms = new MemoryStream(); + MemoryStream ms = new(); try { await Task.Run(() => assemblyDef.Write(ms), token); - ms.Seek(0, SeekOrigin.Begin); + _ = ms.Seek(0, SeekOrigin.Begin); return ms; } catch @@ -130,24 +131,24 @@ public static async Task AsStreamAsync(this AssemblyDef assemblyDef, Can public static MDToken? FindUserStringToken(this AssemblyDef assemblyDef, string value) { - var usStream = (assemblyDef.ManifestModule as ModuleDefMD)! + dnlib.DotNet.MD.USStream usStream = (assemblyDef.ManifestModule as ModuleDefMD)! .Metadata.USStream; - var usReader = usStream.CreateReader(); + DataReader usReader = usStream.CreateReader(); usReader.Position++; // First byte is zero - + uint lenght = usStream.StreamLength; uint rawToken = 1; // The RID starts in 1 while (lenght > 0) { - var pos = usReader.Position; + uint pos = usReader.Position; string str = usReader.ReadUserStringHeap(); if (str.Length > 0 && str == value) { - rawToken = 0x70 << 24 | rawToken; + rawToken = (0x70 << 24) | rawToken; return new MDToken(rawToken); } - lenght -= (uint)(usReader.Position - pos); + lenght -= usReader.Position - pos; rawToken++; } @@ -164,11 +165,7 @@ private static string ReadUserStringHeap(this ref DataReader reader) } string str = reader.ReadString(strLen, Encoding.Unicode); byte terminator = reader.ReadByte(); - if (terminator != GetStringTerminalByte(str)) - { - throw new Exception("Unexpected byte found in user string heap terminator"); - } - return str; + return terminator != GetStringTerminalByte(str) ? throw new Exception("Unexpected byte found in user string heap terminator") : str; // ECMA 335 - II.24.2.4 static byte GetStringTerminalByte(string str) @@ -180,9 +177,9 @@ static byte GetStringTerminalByte(string str) return 0x01; } byte low = (byte)(c & 0xFF); - if ((low >= 0x01 && low <= 0x08) || - (low >= 0x0E && low <= 0x1F) || - low == 0x27 || low == 0x2D || low == 0x7F) + if (low is (>= 0x01 and <= 0x08) or + (>= 0x0E and <= 0x1F) or + 0x27 or 0x2D or 0x7F) { return 0x01; } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsNative.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsNative.cs similarity index 74% rename from Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsNative.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsNative.cs index 8d0c4cc..8a7b4f8 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsNative.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsNative.cs @@ -1,11 +1,11 @@ using System.Runtime.InteropServices; -namespace PatreonPatcher.Helpers; +namespace PatreonPatcher.Core.Helpers; -static class WindowsNative +internal static class WindowsNative { [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern int MessageBoxA(IntPtr hWnd, string lpText, string lpCaption, uint uType); + public static extern int MessageBoxA(nint hWnd, string lpText, string lpCaption, uint uType); [DllImport("comdlg32.dll", SetLastError = true, CharSet = CharSet.Ansi)] public static extern bool GetOpenFileNameA(ref OPENFILENAMEA lpofn); @@ -14,8 +14,8 @@ static class WindowsNative public struct OPENFILENAMEA { public uint lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; + public nint hwndOwner; + public nint hInstance; public string lpstrFilter; public string lpstrCustomFilter; public uint nMaxCustFilter; @@ -30,10 +30,10 @@ public struct OPENFILENAMEA public ushort nFileOffset; public ushort nFileExtension; public string lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; + public nint lCustData; + public nint lpfnHook; public string lpTemplateName; - public IntPtr pvReserved; + public nint pvReserved; public uint dwReserved; public uint FlagsEx; } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsUtils.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsUtils.cs similarity index 65% rename from Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsUtils.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsUtils.cs index 10fbef0..bfcbde5 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Helpers/WindowsUtils.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Helpers/WindowsUtils.cs @@ -1,12 +1,12 @@ using System.Runtime.InteropServices; -namespace PatreonPatcher.Helpers; +namespace PatreonPatcher.Core.Helpers; -static class WindowsUtils +internal static class WindowsUtils { public static string? ShowOpenFileDialog(string filter) { - var ofn = new WindowsNative.OPENFILENAMEA + WindowsNative.OPENFILENAMEA ofn = new() { lStructSize = (uint)Marshal.SizeOf(), lpstrFilter = filter, @@ -16,12 +16,7 @@ static class WindowsUtils Flags = 0x00000008 | 0x00080000 | 0x00001000 }; - if (WindowsNative.GetOpenFileNameA(ref ofn)) - { - return ofn.lpstrFile; - } - - return null; + return WindowsNative.GetOpenFileNameA(ref ofn) ? ofn.lpstrFile : null; } public static string? ShowOpenFileDialog() @@ -31,6 +26,6 @@ static class WindowsUtils public static bool ShowOkCancelMessageBox(string message, string caption) { - return WindowsNative.MessageBoxA(IntPtr.Zero, message, caption, 0x00000001 | 0x00000020) == 1; + return WindowsNative.MessageBoxA(nint.Zero, message, caption, 0x00000001 | 0x00000020) == 1; } } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/IPatchVersion.cs b/Tools/AuthPatcher/PatreonPatcher/Core/IPatchVersion.cs similarity index 79% rename from Tools/AuthPatcher/PatreonPatcher/src/IPatchVersion.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/IPatchVersion.cs index 7326141..c6cdd3b 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/IPatchVersion.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/IPatchVersion.cs @@ -1,6 +1,6 @@ -namespace PatreonPatcher; +namespace PatreonPatcher.Core; -abstract class IPatchVersion : IEquatable +internal abstract class IPatchVersion : IEquatable { public abstract Guid Id { get; } public abstract int Minor { get; } @@ -9,11 +9,7 @@ abstract class IPatchVersion : IEquatable public virtual bool Equals(IPatchVersion? other) { - if (other is null) - { - return false; - } - return other.GetHashCode() == GetHashCode(); + return other is not null && other.GetHashCode() == GetHashCode(); } public override int GetHashCode() diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/LocalPathAssemblyResolver.cs b/Tools/AuthPatcher/PatreonPatcher/Core/LocalPathAssemblyResolver.cs new file mode 100644 index 0000000..c4a86f6 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/LocalPathAssemblyResolver.cs @@ -0,0 +1,27 @@ +using dnlib.DotNet; +using PatreonPatcher.Core.Helpers; + +namespace PatreonPatcher.Core; + +internal class LocalPathAssemblyResolver : IAssemblyResolver +{ + private readonly string basePath; + + public LocalPathAssemblyResolver(string basePath) + { + this.basePath = basePath; + } + + public AssemblyDef? Resolve(IAssembly assembly, ModuleDef sourceModule) + { + string path = Path.Combine(basePath, assembly.Name + ".dll"); + if (!File.Exists(path)) + { + return null; + } + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + System.Reflection.AssemblyName assemblyName = Utils.GetAssemblyName(stream); + return assembly.Version == assemblyName.Version ? AssemblyDef.Load(stream, new ModuleContext(this)) : null; + } +} + diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILogger.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILogger.cs new file mode 100644 index 0000000..9f5495b --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILogger.cs @@ -0,0 +1,6 @@ +namespace PatreonPatcher.Core.Logging; + +internal interface ILogger +{ + void Log(LogLevel level, string message); +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILoggerSink.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILoggerSink.cs new file mode 100644 index 0000000..42d73b3 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/ILoggerSink.cs @@ -0,0 +1,7 @@ +namespace PatreonPatcher.Core.Logging; + +internal interface ILoggerSink : IDisposable +{ + LogLevel LogLevel { get; } + void Write(LogLevel level, string message); +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Log.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Log.cs new file mode 100644 index 0000000..d656e04 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Log.cs @@ -0,0 +1,196 @@ +using System.Collections; +using System.Text; + +namespace PatreonPatcher.Core.Logging; + +internal static class Log +{ + private static ILogger? _logger; + + public static ILogger Logger + { + get + { + if (_logger == null) + { + Logger logger = new(); + _ = Interlocked.CompareExchange(ref _logger, logger, null); + } + return _logger; + } + set + { + ArgumentNullException.ThrowIfNull(value); + _ = Interlocked.Exchange(ref _logger, value); + } + } + + public static void WriteLog(LogLevel level, string message) + { + Logger.Log(level, message); + } + + public static void WriteLog(LogLevel level, string message, params object[] args) + { + object[] stringyObjects = [.. args.Select(Stringify)]; + Logger.Log(level, string.Format(message, args: stringyObjects)); + } + + public static void Info(string message) + { + WriteLog(LogLevel.Info, message); + } + + public static void Info(string message, params object[] args) + { + WriteLog(LogLevel.Info, message, args); + } + + public static void Debug(string message) + { + WriteLog(LogLevel.Debug, message); + } + + public static void Debug(string message, params object[] args) + { + WriteLog(LogLevel.Debug, message, args); + } + + public static void Warning(string message) + { + WriteLog(LogLevel.Warning, message); + } + + public static void Warning(string message, params object[] args) + { + WriteLog(LogLevel.Warning, message, args); + } + + public static void Error(string message) + { + WriteLog(LogLevel.Error, message); + } + + public static void Error(string message, params object[] args) + { + WriteLog(LogLevel.Error, message, args); + } + + private static string Stringify(object obj) + { + if (obj is string str) + { + return str; + } + else if (obj is ICollection collection) + { + return StringifyCollection(collection); + } + else if (obj is IDictionaryEnumerator dictionary) + { + return StringifyDictionary(dictionary); + } + return obj.ToString() ?? obj.GetType().ToString(); + } + + public static string StringifyCollection(ICollection collection) + { + return collection is IDictionary dictionary + ? StringifyDictionary(dictionary) + : collection is IList list ? StringifyList(list) : StringifyEnumerable(collection); + } + + public static string StringifyDictionary(IDictionary dictionary) + { + StringBuilder sb = new(); + _ = sb.Append('{'); + _ = StringifyEnumerable(dictionary, sb); + _ = sb.Append('}'); + return sb.ToString(); + } + + public static string StringifyDictionary(IDictionaryEnumerator dictionary) + { + if (dictionary.Current is not null) + { + return StringifyDictionaryEntry(dictionary); + } + StringBuilder sb = new(); + _ = sb.Append('{'); + StringifyEnumerator(dictionary, sb, (e) => e); + _ = sb.Append('}'); + return sb.ToString(); + } + + public static string StringifyDictionaryEntry(IDictionaryEnumerator dictionary) + { + if (dictionary.Current is null) + { + return StringifyDictionary(dictionary); + } + StringBuilder sb = new(); + _ = sb.Append(dictionary.Key); + _ = sb.Append(": "); + _ = sb.Append(dictionary.Value); + return sb.ToString(); + } + + public static string StringifyList(IList collection) + { + StringBuilder sb = new(); + _ = sb.Append('['); + _ = StringifyEnumerable(collection, sb); + _ = sb.Append(']'); + return sb.ToString(); + } + + public static string StringifyEnumerable(IEnumerable enumerable) + { + IEnumerator enumerator = enumerable.GetEnumerator(); + return enumerator is IDictionaryEnumerator dictionaryEnumerator + ? StringifyEnumerator(dictionaryEnumerator, (e) => e) + : StringifyEnumerator(enumerator); + } + + public static string StringifyEnumerable(IEnumerable enumerable, StringBuilder sb) + { + IEnumerator enumerator = enumerable.GetEnumerator(); + if (enumerator is IDictionaryEnumerator dictionaryEnumerator) + { + StringifyEnumerator(dictionaryEnumerator, sb, (e) => e); + } + else + { + StringifyEnumerator(enumerator, sb); + } + return sb.ToString(); + } + + private static void StringifyEnumerator( + T enumerator, + StringBuilder sb, + Func? valueGetter = null) where T : IEnumerator + { + valueGetter ??= (T obj) => obj.Current; + + if (enumerator.MoveNext()) + { + object value = valueGetter(enumerator); + _ = sb.Append(Stringify(value)); + } + while (enumerator.MoveNext()) + { + _ = sb.Append(", "); + _ = sb.Append(Stringify(valueGetter(enumerator))); + } + } + + private static string StringifyEnumerator( + T enumerator, + Func? valueGetter = null) where T : IEnumerator + { + StringBuilder sb = new(); + StringifyEnumerator(enumerator, sb, valueGetter); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LogLevel.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LogLevel.cs new file mode 100644 index 0000000..0d4357c --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LogLevel.cs @@ -0,0 +1,9 @@ +namespace PatreonPatcher.Core.Logging; + +internal enum LogLevel +{ + Debug, + Info, + Warning, + Error +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Logger.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Logger.cs new file mode 100644 index 0000000..5ebb5fc --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Logger.cs @@ -0,0 +1,54 @@ +namespace PatreonPatcher.Core.Logging; + +internal sealed class Logger : ILogger, IDisposable +{ + public LogLevel LogLevel { get; set; } = LogLevel.Debug; + + private readonly List _writers = []; + + public void AddSynk(ILoggerSink writer) + { + _writers.Add(writer); + } + + public void RemoveSink(ILoggerSink writer) + { + _ = _writers.Remove(writer); + } + + public void RemoveSinksBy(Type loggerType) + { + List writers = _writers.FindAll(w => w.GetType() == loggerType) + .ToList(); + foreach (ILoggerSink? item in writers) + { + RemoveSink(item); + } + } + + public void Log(LogLevel level, string message) + { + if (_writers.Count == 0) + { + return; + } + if (level < LogLevel) + { + return; + } + IEnumerable writers = _writers + .Where(x => x.LogLevel <= level); + foreach (ILoggerSink? writer in writers) + { + writer.Write(level, message); + } + } + + public void Dispose() + { + foreach (ILoggerSink writer in _writers) + { + writer.Dispose(); + } + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LoggerExtensions.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LoggerExtensions.cs new file mode 100644 index 0000000..b38e4fa --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/LoggerExtensions.cs @@ -0,0 +1,58 @@ +using PatreonPatcher.Core.Logging.Sinks; + +namespace PatreonPatcher.Core.Logging; + +internal static class LoggerExtensions +{ + public static void Log(this ILogger logger, LogLevel level, string message) + { + logger.Log(level, message); + } + + public static void LogInfo(this ILogger logger, string message) + { + logger.Log(LogLevel.Info, message); + } + + public static void LogDebug(this ILogger logger, string message) + { + logger.Log(LogLevel.Debug, message); + } + + public static void LogError(this ILogger logger, string message) + { + logger.Log(LogLevel.Error, message); + } + + public static void LogWarning(this ILogger logger, string message) + { + logger.Log(LogLevel.Warning, message); + } + + public static Logger AddFileLogging(this Logger logger, string filePath, int maxSizeMB = 10, LogLevel logLevel = LogLevel.Info) + { + FileLoggerSink fileLogger = new(filePath, maxSizeBytes: 1024 * 1024 * maxSizeMB) + { + LogLevel = logLevel + }; + logger.AddSynk(fileLogger); + return logger; + } + + public static Logger AddConsoleLogging(this Logger logger, LogLevel logLevel = LogLevel.Info) + { + ConsoleLoggerSink consoleLogger = new() + { + LogLevel = logLevel + }; + logger.AddSynk(consoleLogger); + return logger; + } + + public static Logger AddOutputSink(this Logger logger, ILoggerSink sink) + { + logger.AddSynk(sink); + return logger; + } + +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/ConsoleLoggerSink.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/ConsoleLoggerSink.cs new file mode 100644 index 0000000..c4a507d --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/ConsoleLoggerSink.cs @@ -0,0 +1,26 @@ +namespace PatreonPatcher.Core.Logging.Sinks; + +internal class ConsoleLoggerSink : ILoggerSink +{ + public LogLevel LogLevel { get; set; } + + private static readonly Dictionary Colors = new() + { + [LogLevel.Debug] = ConsoleColor.Blue, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warning] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red + }; + + public void Write(LogLevel level, string message) + { + ConsoleColor color = Console.ForegroundColor; + Console.ForegroundColor = Colors[level]; + Console.WriteLine($"[{level}] {message}"); + Console.ForegroundColor = color; + } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/FileLoggerSink.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/FileLoggerSink.cs new file mode 100644 index 0000000..fceb0c3 --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/FileLoggerSink.cs @@ -0,0 +1,60 @@ +using System.Text; + +namespace PatreonPatcher.Core.Logging.Sinks; + +internal class FileLoggerSink : ILoggerSink +{ + private readonly string _filePath; + private readonly int _maxSizeBytes; + private readonly StreamWriter _writer; + + public LogLevel LogLevel { get; set; } = LogLevel.Debug; + + public FileLoggerSink(string filePath, int maxSizeBytes = -1) + { + _filePath = filePath; + _maxSizeBytes = maxSizeBytes; + _writer = new StreamWriter(filePath, encoding: Encoding.UTF8, new FileStreamOptions() + { + Mode = FileMode.Append, + Access = FileAccess.Write, + Share = FileShare.Read, + }); + } + + public void Write(LogLevel level, string message) + { + _writer.WriteLine($"[{level}] {message}"); + } + + public void Dispose() + { + _writer.Flush(); + _writer.Dispose(); + if (File.Exists(_filePath)) + { + FileInfo fileInfo = new(_filePath); + if (_maxSizeBytes < 0 || fileInfo.Length <= _maxSizeBytes) + { + return; + } + long excessBytes = fileInfo.Length - _maxSizeBytes; + FileStream fs = new(_filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + using MemoryStream ms = new(); + try + { + _ = fs.Seek(excessBytes, SeekOrigin.Begin); + fs.CopyTo(ms); + fs.Close(); + fs = File.Create(_filePath); + _ = ms.Seek(0, SeekOrigin.Begin); + ms.CopyTo(fs); + fs.Close(); + } + finally + { + fs.Dispose(); + } + } + } +} diff --git a/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/NullLoggerSink.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/NullLoggerSink.cs new file mode 100644 index 0000000..b97682a --- /dev/null +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Logging/Sinks/NullLoggerSink.cs @@ -0,0 +1,9 @@ +namespace PatreonPatcher.Core.Logging.Sinks; + +internal class NullLoggerSink : ILoggerSink +{ + public LogLevel LogLevel { get; set; } = LogLevel.Debug; + + public void Write(LogLevel level, string message) { } + public void Dispose() { } +} diff --git a/Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.Builder.cs b/Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.Builder.cs similarity index 82% rename from Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.Builder.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.Builder.cs index 31c9481..a2c852a 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.Builder.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.Builder.cs @@ -1,11 +1,11 @@ using dnlib.DotNet; using dnlib.DotNet.Emit; -namespace PatreonPatcher; +namespace PatreonPatcher.Core; internal partial class PatchVersionAttribute { - class Builder + private class Builder { private readonly ModuleDef module; @@ -30,11 +30,11 @@ public TypeDef CreateAttributeType() private void BuildAttributeType() { - var attrbBaseType = module.CorLibTypes.GetTypeRef(nameof(System), nameof(Attribute)); + TypeRef attrbBaseType = module.CorLibTypes.GetTypeRef(nameof(System), nameof(Attribute)); if (!module.GetTypeRefs() .Any(x => x.ReflectionFullName == attrbBaseType.ReflectionFullName)) { - module.Import(attrbBaseType); + _ = module.Import(attrbBaseType); } patchedAttbType = new TypeDefUser(Constants.PatchAttributeNamespace, Constants.PatchAttributeTypeName, module.CorLibTypes.Object.TypeDefOrRef) @@ -64,11 +64,11 @@ private void BuildConstructor() { throw new InvalidOperationException("Attribute type is not created yet"); } - var attrbBaseType = module.CorLibTypes.GetTypeRef(nameof(System), nameof(Attribute)); - var attibuteCtor = new MemberRefUser(module, ".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void), attrbBaseType); + TypeRef attrbBaseType = module.CorLibTypes.GetTypeRef(nameof(System), nameof(Attribute)); + MemberRefUser attibuteCtor = new(module, ".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void), attrbBaseType); - var ctorArgs = new[] { module.CorLibTypes.String, module.CorLibTypes.Int32, module.CorLibTypes.Int32, module.CorLibTypes.Int32 }; - var patchedAttbCtor = new MethodDefUser(".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void, ctorArgs), + CorLibTypeSig[] ctorArgs = new[] { module.CorLibTypes.String, module.CorLibTypes.Int32, module.CorLibTypes.Int32, module.CorLibTypes.Int32 }; + MethodDefUser patchedAttbCtor = new(".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void, ctorArgs), MethodImplAttributes.IL | MethodImplAttributes.Managed, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.ReuseSlot) { @@ -95,11 +95,14 @@ private void BuildConstructor() patchedAttbType.Methods.Add(patchedAttbCtor); } - private static string GetBackingFieldName(string propertyName) => $"<{propertyName}>k__BackingField"; + private static string GetBackingFieldName(string propertyName) + { + return $"<{propertyName}>k__BackingField"; + } private static FieldDefUser CreateBackingField(string name, TypeDefUser type, CorLibTypeSig fieldSiginature) { - var field = new FieldDefUser(GetBackingFieldName(name), new FieldSig(fieldSiginature), FieldAttributes.Private | FieldAttributes.InitOnly); + FieldDefUser field = new(GetBackingFieldName(name), new FieldSig(fieldSiginature), FieldAttributes.Private | FieldAttributes.InitOnly); type.Fields.Add(field); return field; } @@ -107,7 +110,7 @@ private static FieldDefUser CreateBackingField(string name, TypeDefUser type, Co private static void CreateProperty(string name, TypeDefUser type, CorLibTypeSig propertySignature, ref FieldDefUser? backingField) { backingField ??= CreateBackingField(name, type, propertySignature); - var property = new PropertyDefUser(name, new PropertySig(true, propertySignature)) + PropertyDefUser property = new(name, new PropertySig(true, propertySignature)) { GetMethod = new MethodDefUser($"get_{name}", MethodSig.CreateInstance(propertySignature), MethodImplAttributes.IL | MethodImplAttributes.Managed, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.ReuseSlot) diff --git a/Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.cs b/Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.cs similarity index 86% rename from Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.cs index 87fcf8f..855eeec 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/PatchVersionAttribute.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/PatchVersionAttribute.cs @@ -1,6 +1,6 @@ using dnlib.DotNet; -namespace PatreonPatcher; +namespace PatreonPatcher.Core; internal partial class PatchVersionAttribute : IPatchVersion { @@ -36,14 +36,14 @@ private PatchVersionAttribute(Guid id, int major, int minor, int patch) public static CustomAttribute Create(ModuleDef module, string patchId, int major, int minor, int patch) { - var attributeRef = new TypeRefUser(module, Constants.PatchAttributeNamespace, Constants.PatchAttributeTypeName); - var typeDef = attributeRef.Resolve(); + TypeRefUser attributeRef = new(module, Constants.PatchAttributeNamespace, Constants.PatchAttributeTypeName); + TypeDef? typeDef = attributeRef.Resolve(); if (typeDef is null) { - var builder = new Builder(module); + Builder builder = new(module); typeDef = builder.CreateAttributeType(); } - var ctor = typeDef.FindConstructors().First(); + MethodDef ctor = typeDef.FindConstructors().First(); return Create(ctor, module, patchId, major, minor, patch); } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Patcher.cs b/Tools/AuthPatcher/PatreonPatcher/Core/Patcher.cs similarity index 58% rename from Tools/AuthPatcher/PatreonPatcher/src/Patcher.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/Patcher.cs index 2be379a..bc73eba 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/Patcher.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/Patcher.cs @@ -1,10 +1,11 @@ using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.PE; -using PatreonPatcher.Helpers; +using PatreonPatcher.Core.Helpers; +using PatreonPatcher.Core.Logging; using System.Diagnostics.CodeAnalysis; -namespace PatreonPatcher; +namespace PatreonPatcher.Core; internal class Patcher { @@ -14,10 +15,10 @@ internal class Patcher private readonly string _assembliesPath; private bool _methodsLoaded = false; - MethodDef? _writeAuthFunction; - MethodDef? _invokeSuccessFunction; - MethodDef? _bypassAuthFunction; - MethodDef? _awakeFunction; + private MethodDef? _writeAuthFunction; + private MethodDef? _invokeSuccessFunction; + private MethodDef? _bypassAuthFunction; + private MethodDef? _awakeFunction; private Patcher(string assembliesPath, ModuleContext moduleContext) { @@ -30,13 +31,13 @@ public async Task PatchAsync() { if (!LoadMethods()) { - Logger.Error("Failed to load methods."); + Log.Error("Failed to load methods."); return false; } if (IsPatched()) { - Logger.Info("Game already patched."); + Log.Info("Game already patched."); return true; } @@ -47,13 +48,13 @@ public async Task PatchAsync() OpCodes.Ret.ToInstruction() ]; - var awakeIlCode = new LinkedList(_awakeFunction!.Body.Instructions); - var ip = awakeIlCode.First; + LinkedList awakeIlCode = new(_awakeFunction!.Body.Instructions); + LinkedListNode? ip = awakeIlCode.First; while (ip != null) { if (ip.Value.OpCode == OpCodes.Ret) { - Logger.Info($"Patching ret instruction at {_awakeFunction.RVA:X}[{ip.Value.GetOffset():X}]"); + Log.Info($"Patching ret instruction at {_awakeFunction.RVA:X}[{ip.Value.GetOffset():X}]"); AddInstructionsAfter(ip.Previous!, callBypassAuth); awakeIlCode.Remove(ip); break; @@ -61,37 +62,37 @@ public async Task PatchAsync() ip = ip.Next; void AddInstructionsAfter(LinkedListNode target, Instruction[] instructions) { - foreach (var instr in instructions) + foreach (Instruction instr in instructions) { - awakeIlCode.AddAfter(target, instr); + _ = awakeIlCode.AddAfter(target, instr); target = target.Next!; } } } _awakeFunction.Body.Instructions.Clear(); - foreach (var instr in awakeIlCode) + foreach (Instruction instr in awakeIlCode) { _awakeFunction.Body.Instructions.Add(instr); } - var assembly = _writeAuthFunction!.DeclaringType.DefinitionAssembly as AssemblyDef + AssemblyDef assembly = _writeAuthFunction!.DeclaringType.DefinitionAssembly as AssemblyDef ?? throw new Exception("Failed to get auth assembly"); - var attb = PatchVersionAttribute.Create(assembly.ManifestModule, patchId.ToString(), 0, 0, 0); + CustomAttribute attb = PatchVersionAttribute.Create(assembly.ManifestModule, patchId.ToString(), 0, 0, 0); assembly.CustomAttributes.Add(attb); - var assemblyName = assembly.Name + ".dll"; + string assemblyName = assembly.Name + ".dll"; - var assemblyPath = GetAssemblyPath(assemblyName); + string assemblyPath = GetAssemblyPath(assemblyName); File.Move(assemblyPath, assemblyPath + ".bak"); try { - Logger.Info($"Writing patched assembly to {assemblyPath}"); + Log.Info($"Writing patched assembly to {assemblyPath}"); await Task.Run(() => assembly.Write(assemblyPath)); } catch (Exception) { - Logger.Error("Failed to write patched assembly. Restoring backup."); + Log.Error("Failed to write patched assembly. Restoring backup."); File.Move(assemblyPath + ".bak", assemblyPath); throw; } @@ -104,19 +105,20 @@ private bool LoadMethods() { return true; } + Log.Debug($"Loading methods from {_assembliesPath}"); - var modules = Directory.GetFiles(_assembliesPath, "*.dll"); - Logger.Info($"Found {modules.Length} assemblies in {_assembliesPath}"); + string[] modules = Directory.GetFiles(_assembliesPath, "*.dll"); + Log.Info($"Found {modules.Length} assemblies in {_assembliesPath}"); Stream? authAssemblyStream = null; MethodDef? invokeSuccessMethod = null; - var options = new ParallelOptions() + ParallelOptions options = new() { MaxDegreeOfParallelism = 2 }; - Parallel.ForEach(modules, options, (modulePath, loop) => + _ = Parallel.ForEach(modules, options, (modulePath, loop) => { - var assembly = OpenAssembly(modulePath); + Stream assembly = OpenAssembly(modulePath); int? rva = Utils.Pattern.FindMethodRVAMatchingPattern(assembly, Constants.Patterns.InvokeSuccessFunction); if (rva is null) @@ -125,8 +127,8 @@ private bool LoadMethods() return; } - var assemblyDef = AssemblyDef.Load(assembly, _context); - var methodDef = assemblyDef.ManifestModule + AssemblyDef assemblyDef = AssemblyDef.Load(assembly, _context); + MethodDef methodDef = assemblyDef.ManifestModule .GetTypes() .SelectMany(x => x.Methods) .Single(x => x.RVA == (RVA)rva.Value); @@ -138,7 +140,7 @@ private bool LoadMethods() invokeSuccessMethod = methodDef; authAssemblyStream = assembly; loop.Break(); - Logger.Info($"Found {invokeSuccessMethod.FullName} at {invokeSuccessMethod.RVA:X}"); + Log.Info($"Found {invokeSuccessMethod.FullName} at {invokeSuccessMethod.RVA:X}"); } else { @@ -149,48 +151,47 @@ private bool LoadMethods() if (invokeSuccessMethod is null || authAssemblyStream is null) { - Logger.Error("Failed to find InvokeSuccessFunction"); + Log.Error("Failed to find InvokeSuccessFunction"); return false; } - var authType = invokeSuccessMethod.DeclaringType; - var authAssembly = authType.DefinitionAssembly as AssemblyDef; + TypeDef authType = invokeSuccessMethod.DeclaringType; + AssemblyDef? authAssembly = authType.DefinitionAssembly as AssemblyDef; - uint playerPrefsHasKeyToken = Utils.FindMethodRefToken( - authAssembly!, + uint playerPrefsHasKeyToken = authAssembly!.FindMethodRefToken( Constants.UnityEngineTypes.PlayerPrefs, Constants.UnityEngineTypes.PlayerPrefs_HasKey)!.Value.Raw; - Logger.Info($"Found PlayerPrefs.HasKey token: {playerPrefsHasKeyToken:X}"); + Log.Info($"Found PlayerPrefs.HasKey token: {playerPrefsHasKeyToken:X}"); - var hwidStringToken = Utils.FindUserStringToken(authAssembly!, "HWID")!.Value.Raw; - Logger.Info($"Found HWID string token: {hwidStringToken:X}"); + uint hwidStringToken = authAssembly!.FindUserStringToken("HWID")!.Value.Raw; + Log.Info($"Found HWID string token: {hwidStringToken:X}"); - var patternBuilder = new PatternBuilder(Constants.Patterns.WriteAuthFunction); + PatternBuilder patternBuilder = new(Constants.Patterns.WriteAuthFunction); string writeAuthPattern = patternBuilder.Render(new Dictionary() { { "token", Utils.SwapEndianness(playerPrefsHasKeyToken) }, { "string_token", Utils.SwapEndianness(hwidStringToken) } }); - var writeAuthMethod = Utils.Pattern.FindTypeMethodMatchingPattern(authAssemblyStream, authType, writeAuthPattern); + MethodDef? writeAuthMethod = Utils.Pattern.FindTypeMethodMatchingPattern(authAssemblyStream, authType, writeAuthPattern); if (writeAuthMethod is null) { - Logger.Error("Failed to find WriteAuthFunction"); + Log.Error("Failed to find WriteAuthFunction"); authAssemblyStream.Dispose(); return false; } - Logger.Info($"Found {writeAuthMethod.FullName} at {writeAuthMethod.RVA:X}"); + Log.Info($"Found {writeAuthMethod.FullName} at {writeAuthMethod.RVA:X}"); - var awakeMethod = authType.FindMethod(Constants.UnityEngineTypes.MonoBehaviour_Awake); + MethodDef? awakeMethod = authType.FindMethod(Constants.UnityEngineTypes.MonoBehaviour_Awake); if (awakeMethod is null) { - Logger.Error("Failed to find Awake method"); + Log.Error("Failed to find Awake method"); return false; } - Logger.Info($"Found {awakeMethod.FullName} at {awakeMethod.RVA:X}"); + Log.Info($"Found {awakeMethod.FullName} at {awakeMethod.RVA:X}"); - var bypassAuthFunctions = Utils.Pattern.FindTypeMethodsMatchingPattern(authAssemblyStream, authType, Constants.Patterns.BypassAuthFunction); + MethodDef[] bypassAuthFunctions = Utils.Pattern.FindTypeMethodsMatchingPattern(authAssemblyStream, authType, Constants.Patterns.BypassAuthFunction); if (bypassAuthFunctions.Length == 0) { - Logger.Error("Failed to find BypassAuthFunction"); + Log.Error("Failed to find BypassAuthFunction"); authAssemblyStream.Dispose(); return false; } @@ -199,9 +200,9 @@ private bool LoadMethods() authAssemblyStream.Dispose(); MethodDef? bypassAuthMethod = null; - foreach (var method in bypassAuthFunctions) + foreach (MethodDef method in bypassAuthFunctions) { - var isCorrectMethod = method.Body.Instructions + bool isCorrectMethod = method.Body.Instructions .Where(x => x.OpCode == OpCodes.Call && x.Operand is IMethodDefOrRef) .Select(x => (x.Operand as IMethodDefOrRef).ResolveMethodDef()) @@ -211,17 +212,18 @@ private bool LoadMethods() if (isCorrectMethod) { bypassAuthMethod = method; - Logger.Info($"Found {bypassAuthMethod.FullName} at {bypassAuthMethod.RVA:X}"); + Log.Info($"Found {bypassAuthMethod.FullName} at {bypassAuthMethod.RVA:X}"); break; } } if (bypassAuthMethod is null) { - Logger.Error("Failed to find BypassAuthFunction"); + Log.Error("Failed to find BypassAuthFunction"); return false; } + Log.Debug("Methods loaded successfully"); _writeAuthFunction = writeAuthMethod; _invokeSuccessFunction = invokeSuccessMethod; _bypassAuthFunction = bypassAuthMethod; @@ -233,18 +235,23 @@ private bool LoadMethods() [RequiresDynamicCode("Calls PatreonPatcher.src.Patcher.LoadMethods()")] private bool IsPatched() { + Log.Debug("Checking if game is patched"); if (!_methodsLoaded && !LoadMethods()) { - Logger.Error("Failed to load methods."); + Log.Error("Failed to load methods."); return false; } if (_writeAuthFunction!.DeclaringType.DefinitionAssembly is not AssemblyDef assembly) { - Logger.Error("Failed to get AssemblyDef from the auth assembly"); + Log.Error("Failed to get AssemblyDef from the auth assembly"); return false; } - var version = PatchVersionAttribute.GetPatchVersion(assembly, patchId); + IPatchVersion? version = PatchVersionAttribute.GetPatchVersion(assembly, patchId); + if (version is not null) + { + Log.Debug($"Game is patched with version {version.Major}.{version.Minor}.{version.Patch}"); + } return version != null; } @@ -253,13 +260,13 @@ private string GetAssemblyPath(string assemblyName) return Path.Combine(_assembliesPath, assemblyName); } - private Stream OpenAssembly(string assemblyName) + private FileStream OpenAssembly(string assemblyName) { if (!Path.IsPathFullyQualified(assemblyName)) { assemblyName = GetAssemblyPath(assemblyName); } - var fs = new FileStream(assemblyName, FileMode.Open, FileAccess.Read, FileShare.Read); + FileStream fs = new(assemblyName, FileMode.Open, FileAccess.Read, FileShare.Read); return fs; } @@ -273,20 +280,20 @@ public static Patcher Create(string gameExecutable) string gameBaseDirectory = Path.GetDirectoryName(gameExecutable)!; string exeName = Path.GetFileNameWithoutExtension(gameExecutable); - var assembliesDirectory = Path.Combine(gameBaseDirectory, exeName + Constants.Directories.AssembliesDirectory); + string assembliesDirectory = Path.Combine(gameBaseDirectory, exeName + Constants.Directories.AssembliesDirectory); if (!Directory.Exists(assembliesDirectory)) { throw new DirectoryNotFoundException($"Assemblies directory not found at {assembliesDirectory}"); } - var unityEngineAssemblyPath = Path.Combine(assembliesDirectory, Constants.UnityEngineAssembly); + string unityEngineAssemblyPath = Path.Combine(assembliesDirectory, Constants.UnityEngineAssembly); if (!File.Exists(unityEngineAssemblyPath)) { throw new FileNotFoundException($"UnityEngine assembly not found at {unityEngineAssemblyPath}"); } - var resolver = new LocalPathAssemblyResolver(assembliesDirectory); - var moduleContext = new ModuleContext(resolver); + LocalPathAssemblyResolver resolver = new(assembliesDirectory); + ModuleContext moduleContext = new(resolver); return new Patcher(assembliesDirectory, moduleContext); } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/PatternBuilder.cs b/Tools/AuthPatcher/PatreonPatcher/Core/PatternBuilder.cs similarity index 65% rename from Tools/AuthPatcher/PatreonPatcher/src/PatternBuilder.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/PatternBuilder.cs index a986fae..7892808 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/PatternBuilder.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/PatternBuilder.cs @@ -6,32 +6,32 @@ namespace PatreonPatcher; -partial class PatternBuilder +internal partial class PatternBuilder { - readonly StringBuilder _builder = new(); - readonly HashSet symbols = []; + private readonly StringBuilder _builder = new(); + private readonly HashSet symbols = []; public PatternBuilder(string pattern) { - var matches = TemplateRegex.Matches(pattern); + MatchCollection matches = TemplateRegex.Matches(pattern); for (int i = 0; i < matches.Count; i++) { - var match = matches[i]; - var name = match.Groups["name"].Value; - symbols.Add(name); + Match match = matches[i]; + string name = match.Groups["name"].Value; + _ = symbols.Add(name); } - _builder.Append(pattern); - _builder.Replace(" ", ""); + _ = _builder.Append(pattern); + _ = _builder.Replace(" ", ""); } [RequiresUnreferencedCode("This method uses reflection to get property values from the object")] public string Render(object? obj) { - var symbolValues = new Dictionary(); - foreach (var symbol in symbols) + Dictionary symbolValues = []; + foreach (string symbol in symbols) { ArgumentNullException.ThrowIfNull(obj); - var value = (obj.GetType().GetProperty(symbol)?.GetValue(obj)) + object value = (obj.GetType().GetProperty(symbol)?.GetValue(obj)) ?? throw new ArgumentException($"Missing value for symbol '{symbol}'"); symbolValues[symbol] = value; } @@ -40,9 +40,9 @@ public string Render(object? obj) public string Render(Dictionary values) { - foreach (var symbol in symbols) + foreach (string symbol in symbols) { - if (!values.TryGetValue(symbol, out var value)) + if (!values.TryGetValue(symbol, out object? value)) { throw new ArgumentException($"Missing value for symbol '{symbol}'"); } @@ -60,23 +60,30 @@ public string Render(Dictionary values) string s => s, _ => throw new ArgumentException($"Invalid value type for symbol '{symbol}'") }; - static string Align(string value) => value.Length % 2 == 0 ? value : "0" + value; - _builder.Replace($"{{{symbol}}}", Align(formatedValue)); + static string Align(string value) + { + return value.Length % 2 == 0 ? value : "0" + value; + } + + _ = _builder.Replace($"{{{symbol}}}", Align(formatedValue)); } return FormatPattern(_builder); } - public string Render() => Render([]); + public string Render() + { + return Render([]); + } public PatternBuilder Write(T value) where T : INumber { - _builder.Append(value.ToString("X", new NumberFormatInfo())); + _ = _builder.Append(value.ToString("X", new NumberFormatInfo())); return this; } public PatternBuilder WriteWildCard() { - _builder.Append("??"); + _ = _builder.Append("??"); return this; } @@ -85,7 +92,7 @@ private static string FormatPattern(StringBuilder @string) int start = Math.Min(@string.Length, 2); for (int i = start; i < @string.Length; i += 3) { - @string.Insert(i, ' '); + _ = @string.Insert(i, ' '); } return @string.ToString(); } diff --git a/Tools/AuthPatcher/PatreonPatcher/src/PatternScanner.cs b/Tools/AuthPatcher/PatreonPatcher/Core/PatternScanner.cs similarity index 90% rename from Tools/AuthPatcher/PatreonPatcher/src/PatternScanner.cs rename to Tools/AuthPatcher/PatreonPatcher/Core/PatternScanner.cs index dbccf55..293bde3 100644 --- a/Tools/AuthPatcher/PatreonPatcher/src/PatternScanner.cs +++ b/Tools/AuthPatcher/PatreonPatcher/Core/PatternScanner.cs @@ -1,17 +1,17 @@ -namespace PatreonPatcher; +namespace PatreonPatcher.Core; -class PatternScanner +internal class PatternScanner { private readonly ushort[] pattern; public PatternScanner(string pattern) { - var tokens = pattern.Split(' '); + string[] tokens = pattern.Split(' '); this.pattern = new ushort[tokens.Length]; for (int i = 0; i < tokens.Length; i++) { - var token = tokens[i]; + string token = tokens[i]; ushort value; if (token == "??") { @@ -61,7 +61,7 @@ private static bool IsHexByte(ReadOnlySpan span) { return false; } - foreach (var c in span) + foreach (char c in span) { if (!char.IsAsciiHexDigit(c)) { diff --git a/Tools/AuthPatcher/PatreonPatcher/PatreonPatcher.csproj b/Tools/AuthPatcher/PatreonPatcher/PatreonPatcher.csproj index f07f6a9..bc9554b 100644 --- a/Tools/AuthPatcher/PatreonPatcher/PatreonPatcher.csproj +++ b/Tools/AuthPatcher/PatreonPatcher/PatreonPatcher.csproj @@ -12,6 +12,7 @@ + diff --git a/Tools/AuthPatcher/PatreonPatcher/src/LocalPathAssemblyResolver.cs b/Tools/AuthPatcher/PatreonPatcher/src/LocalPathAssemblyResolver.cs deleted file mode 100644 index 3c9420d..0000000 --- a/Tools/AuthPatcher/PatreonPatcher/src/LocalPathAssemblyResolver.cs +++ /dev/null @@ -1,31 +0,0 @@ -using dnlib.DotNet; -using PatreonPatcher.Helpers; - -namespace PatreonPatcher; - -class LocalPathAssemblyResolver : IAssemblyResolver -{ - private readonly string basePath; - - public LocalPathAssemblyResolver(string basePath) - { - this.basePath = basePath; - } - - public AssemblyDef? Resolve(IAssembly assembly, ModuleDef sourceModule) - { - var path = Path.Combine(basePath, assembly.Name + ".dll"); - if (!File.Exists(path)) - { - return null; - } - using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - var assemblyName = Utils.GetAssemblyName(stream); - if (assembly.Version == assemblyName.Version) - { - return AssemblyDef.Load(stream, new ModuleContext(this)); - } - return null; - } -} - diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Logger.cs b/Tools/AuthPatcher/PatreonPatcher/src/Logger.cs deleted file mode 100644 index 005d5fd..0000000 --- a/Tools/AuthPatcher/PatreonPatcher/src/Logger.cs +++ /dev/null @@ -1,45 +0,0 @@ - -namespace PatreonPatcher; - -static class Logger -{ - public static ILoggerWriter Writer { get; set; } = new ConsoleLogger(); - - public static void Log(LogLevel level, string message) - { - Writer.Write(level, message); - } - public static void Info(string message) => Log(LogLevel.Info, message); - public static void Warning(string message) => Log(LogLevel.Warning, message); - public static void Error(string message) => Log(LogLevel.Error, message); - - public interface ILoggerWriter - { - void Write(LogLevel level, string message); - } - - public class ConsoleLogger : ILoggerWriter - { - private static readonly Dictionary Colors = new() - { - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warning] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red - }; - - public void Write(LogLevel level, string message) - { - var color = Console.ForegroundColor; - Console.ForegroundColor = Colors[level]; - Console.WriteLine($"[{level}] {message}"); - Console.ForegroundColor = color; - } - } - - public enum LogLevel - { - Info, - Warning, - Error - } -} \ No newline at end of file diff --git a/Tools/AuthPatcher/PatreonPatcher/src/Program.cs b/Tools/AuthPatcher/PatreonPatcher/src/Program.cs deleted file mode 100644 index 42088d2..0000000 --- a/Tools/AuthPatcher/PatreonPatcher/src/Program.cs +++ /dev/null @@ -1,125 +0,0 @@ -using PatreonPatcher; -using PatreonPatcher.Helpers; -using System.Diagnostics.CodeAnalysis; - -[RequiresDynamicCode("Calls PatreonPatcher.src.Patcher.PatchAsync()")] -internal class Program -{ - private static async Task Main(string[] args) - { - string? gameDirectory = null; - if (args.Length == 1) - { - string path = args[0]; - if (IsValidUnityDirectory(path)) - { - gameDirectory = Path.GetDirectoryName(path); - } - } - - if ((gameDirectory ??= WaitUserSelectGameExecutable()) is null) - { - Logger.Error("Operation canceled by user."); - return; - } - - try - { - var patcher = Patcher.Create(gameDirectory!); - var ok = await patcher.PatchAsync(); - if (ok) - { - ShowDoneMessage(); - } - else - { - ShowErrorMessage(); - } - } - catch (Exception e) - { - Logger.Error("An error occurred while patching: " + e.Message); - Console.WriteLine("=================| STACK TRACE |================="); - Console.WriteLine(e.StackTrace); - Console.WriteLine("=================================================\n\n"); - ShowErrorMessage(); - -#if DEBUG - throw; -#endif - } - } - - static bool IsValidUnityDirectory(string path) - { - if (Path.IsPathFullyQualified(path)) - path = Path.GetDirectoryName(path) ?? string.Empty; - - return File.Exists(Path.Combine(path, Constants.UnityPlayerAssembly)); - } - - static string? WaitUserSelectGameExecutable() - { - string? gameExecutable; - do - { - gameExecutable = WindowsUtils.ShowOpenFileDialog($"AntroHeat.exe\0*.exe\0"); - - if (gameExecutable == null || !IsValidUnityDirectory(gameExecutable)) - { - if (!WindowsUtils.ShowOkCancelMessageBox("Please select the game executable or press cancel.", "Invalid game executable")) - { - return null; - } - gameExecutable = null; - } - } while (gameExecutable == null); - - return gameExecutable; - } - - public static void ShowDoneMessage() - { - var prevColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine( -@" - ____ _ -| _ \ ___ _ __ ___| | -| | | |/ _ \| '_ \ / _ \ | -| |_| | (_) | | | | __/_| -|____/ \___/|_| |_|\___(_) -"); - Console.ForegroundColor = prevColor; - Console.WriteLine("Patching successful!"); - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); - } - - public static void ShowErrorMessage() - { - var prevColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine( -@" - _____ _ _ _ _ - / ____| | | | | (_) | | - | (___ ___ _ __ ___ ___| |_| |__ _ _ __ __ _ __ _____ _ __ | |_ __ ___ __ ___ _ __ __ _ - \___ \ / _ \| '_ ` _ \ / _ \ __| '_ \| | '_ \ / _` | \ \ /\ / / _ \ '_ \| __| \ \ /\ / / '__/ _ \| '_ \ / _` | - ____) | (_) | | | | | | __/ |_| | | | | | | | (_| | \ V V / __/ | | | |_ \ V V /| | | (_) | | | | (_| | - |_____/ \___/|_| |_| |_|\___|\__|_| |_|_|_| |_|\__, | \_/\_/ \___|_| |_|\__| \_/\_/ |_| \___/|_| |_|\__, | - __/ | __/ | - |___/ |___/ -"); - - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine("Sorry for the inconvenience, but the gane could not be patched."); - Console.WriteLine("If you believe this is an error, please open an issue at:"); - Console.ForegroundColor = ConsoleColor.Blue; - Console.WriteLine("https://github.com/OpenYiffGames/HeatGame/issues"); - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine("Press any key to exit..."); - Console.ForegroundColor = prevColor; - Console.ReadKey(); - } -}