diff --git a/YaeAchievement.slnx b/YaeAchievement.slnx index e5fb9f7..dbf91b7 100644 --- a/YaeAchievement.slnx +++ b/YaeAchievement.slnx @@ -1,3 +1,4 @@  + \ No newline at end of file diff --git a/YaeAchievement/res/App.Designer.cs b/YaeAchievement/res/App.Designer.cs index 1cc7302..efc4dc5 100644 --- a/YaeAchievement/res/App.Designer.cs +++ b/YaeAchievement/res/App.Designer.cs @@ -222,7 +222,7 @@ namespace YaeAchievement.res { } /// - /// Looks up a localized string similar to Fail, please contact developer to get help information. + /// Looks up a localized string similar to Fail, please contact developer to get help information (CG_{0}). /// internal static string ExportToCocogoatFail { get { diff --git a/YaeAchievement/res/App.resx b/YaeAchievement/res/App.resx index 02e7386..c6b1ce3 100644 --- a/YaeAchievement/res/App.resx +++ b/YaeAchievement/res/App.resx @@ -19,7 +19,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Fail, please contact developer to get help information + Fail, please contact developer to get help information (CG_{0}) all achievement diff --git a/YaeAchievement/res/App.zh.resx b/YaeAchievement/res/App.zh.resx index 62f3376..1a8f80b 100644 --- a/YaeAchievement/res/App.zh.resx +++ b/YaeAchievement/res/App.zh.resx @@ -12,7 +12,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 导出失败, 请联系开发者以获取帮助 + 导出失败, 请联系开发者以获取帮助(CG_{0}) 全部成就 diff --git a/YaeAchievement/res/proto/AchievementInfo.proto b/YaeAchievement/res/proto/AchievementInfo.proto index 58f238b..9033ad9 100644 --- a/YaeAchievement/res/proto/AchievementInfo.proto +++ b/YaeAchievement/res/proto/AchievementInfo.proto @@ -17,9 +17,22 @@ message AchievementItem { string description = 4; } +message MethodRvaConfig { + uint32 do_cmd = 1; + uint32 to_uint16 = 2; + uint32 update_normal_prop = 3; +} + +message NativeLibConfig { + uint32 store_cmd_id = 1; + uint32 achievement_cmd_id = 2; + map method_rva = 10; +} + message AchievementInfo { string version = 1; map group = 2; map items = 3; AchievementProtoFieldInfo pb_info = 4; + NativeLibConfig native_config = 5; } diff --git a/YaeAchievement/res/proto/UpdateInfo.proto b/YaeAchievement/res/proto/UpdateInfo.proto index a43558b..cab8eaf 100644 --- a/YaeAchievement/res/proto/UpdateInfo.proto +++ b/YaeAchievement/res/proto/UpdateInfo.proto @@ -10,6 +10,4 @@ message UpdateInfo { bool force_update = 5; bool enable_lib_download = 6; bool enable_auto_update = 7; - string current_cn_hash = 8; - string current_os_hash = 9; } diff --git a/YaeAchievement/src/Export.cs b/YaeAchievement/src/Export.cs index f24f6d8..42d54ba 100644 --- a/YaeAchievement/src/Export.cs +++ b/YaeAchievement/src/Export.cs @@ -48,7 +48,7 @@ public static class Export { request.Content = new StringContent(result, Encoding.UTF8, "application/json"); using var response = Utils.CHttpClient.Send(request); if (response.StatusCode != HttpStatusCode.Created) { - AnsiConsole.WriteLine(App.ExportToCocogoatFail); + AnsiConsole.WriteLine(App.ExportToCocogoatFail, response.StatusCode); return; } var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); diff --git a/YaeAchievement/src/Program.cs b/YaeAchievement/src/Program.cs index fa4bacc..370f4a5 100644 --- a/YaeAchievement/src/Program.cs +++ b/YaeAchievement/src/Program.cs @@ -59,7 +59,7 @@ internal static class Program { } } catch (Exception) { /* ignored */ } - if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) { + if (data != null && CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow) { var prompt = new SelectionPromptCompat() .Title(App.UsePreviousData) .AddChoices(App.CommonYes, App.CommonNo); @@ -72,7 +72,7 @@ internal static class Program { StartAndWaitResult(AppConfig.GamePath, new Dictionary> { { 1, AchievementAllDataNotify.OnReceive }, { 2, PlayerStoreNotify.OnReceive }, - { 100, PlayerPropNotify.OnReceive }, + { 3, PlayerPropNotify.OnReceive }, }, () => { #if DEBUG_EX PlayerPropNotify.OnFinish(); diff --git a/YaeAchievement/src/Utilities/Crc32.cs b/YaeAchievement/src/Utilities/Crc32.cs new file mode 100644 index 0000000..c173c5c --- /dev/null +++ b/YaeAchievement/src/Utilities/Crc32.cs @@ -0,0 +1,26 @@ +namespace YaeAchievement.Utilities; + +// CRC-32-IEEE 802.3 +public static class Crc32 { + + private const uint Polynomial = 0xEDB88320; + private static readonly uint[] Crc32Table = new uint[256]; + + static Crc32() { + for (uint i = 0; i < Crc32Table.Length; i++) { + var v = i; + for (var j = 0; j < 8; j++) { + v = (v >> 1) ^ ((v & 1) * Polynomial); + } + Crc32Table[i] = v; + } + } + + public static uint Compute(Span buf) { + var checksum = 0xFFFFFFFF; + foreach (var b in buf) { + checksum = (checksum >> 8) ^ Crc32Table[(b ^ checksum) & 0xFF]; + } + return ~checksum; + } +} diff --git a/YaeAchievement/src/Utils.cs b/YaeAchievement/src/Utils.cs index 2472dc6..fa7e23b 100644 --- a/YaeAchievement/src/Utils.cs +++ b/YaeAchievement/src/Utils.cs @@ -203,30 +203,40 @@ public static class Utils { } private static bool _isUnexpectedExit = true; - + // ReSharper disable once UnusedMethodReturnValue.Global public static void StartAndWaitResult(string exePath, Dictionary> handlers, Action onFinish) { - _proc = new GameProcess(exePath); - _proc.LoadLibrary(GlobalVars.LibFilePath); - _proc.ResumeMainThread(); - _proc.OnExit += () => { - if (_isUnexpectedExit) { - _proc = null; - AnsiConsole.WriteLine(App.GameProcessExit); - Environment.Exit(114514); - } - }; - AnsiConsole.WriteLine(App.GameLoading, _proc.Id); + var hash = GetGameHash(exePath); + var nativeConf = GlobalVars.AchievementInfo.NativeConfig; + if (!nativeConf.MethodRva.TryGetValue(hash, out var methodRva)) { + AnsiConsole.WriteLine($"No match config {exePath} {hash:X8}"); + Environment.Exit(0); + return; + } Task.Run(() => { using var stream = new NamedPipeServerStream(GlobalVars.PipeName); - using var reader = new BinaryReader(stream); + using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, true); + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); stream.WaitForConnection(); int type; while ((type = stream.ReadByte()) != -1) { - if (type == 0xFF) { - _isUnexpectedExit = false; - onFinish(); - break; + switch (type) { + case 0xFC: + writer.Write(nativeConf.AchievementCmdId); + writer.Write(nativeConf.StoreCmdId); + break; + case 0xFD: + writer.Write(methodRva.DoCmd); + writer.Write(methodRva.ToUint16); + writer.Write(methodRva.UpdateNormalProp); + break; + case 0xFE: + _proc!.ResumeMainThread(); + break; + case 0xFF: + _isUnexpectedExit = false; + onFinish(); + return; } if (handlers.TryGetValue(type, out var handler)) { if (handler(reader)) { @@ -235,6 +245,23 @@ public static class Utils { } } }).ContinueWith(task => { if (task.IsFaulted) OnUnhandledException(task.Exception!); }); + _proc = new GameProcess(exePath); + _proc.LoadLibrary(GlobalVars.LibFilePath); + _proc.OnExit += () => { + if (_isUnexpectedExit) { + _proc = null; + AnsiConsole.WriteLine(App.GameProcessExit); + Environment.Exit(114514); + } + }; + AnsiConsole.WriteLine(App.GameLoading, _proc.Id); + } + + private static uint GetGameHash(string exePath) { + Span buffer = stackalloc byte[0x10000]; + using var stream = File.OpenRead(exePath); + _ = stream.Read(buffer); + return Crc32.Compute(buffer); } internal static unsafe void SetQuickEditMode(bool enable) { diff --git a/YaeAchievementLib/YaeAchievementLib.csproj b/YaeAchievementLib/YaeAchievementLib.csproj new file mode 100644 index 0000000..d6e8527 --- /dev/null +++ b/YaeAchievementLib/YaeAchievementLib.csproj @@ -0,0 +1,50 @@ + + + + enable + preview + Yae + enable + true + net9.0-windows + + + + true + x64 + win-x64 + true + Speed + true + + + + + + all + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/YaeAchievementLib/lib/libMinHook.x64.lib b/YaeAchievementLib/lib/libMinHook.x64.lib new file mode 100644 index 0000000..e211ad6 Binary files /dev/null and b/YaeAchievementLib/lib/libMinHook.x64.lib differ diff --git a/YaeAchievementLib/src/Application.cs b/YaeAchievementLib/src/Application.cs new file mode 100644 index 0000000..802f4c7 --- /dev/null +++ b/YaeAchievementLib/src/Application.cs @@ -0,0 +1,134 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Yae.Utilities; + +namespace Yae; + +internal static unsafe class Application { + + [UnmanagedCallersOnly(EntryPoint = "YaeMain")] + private static uint Awake(nint hModule) { + Native.RegisterUnhandledExceptionHandler(); + Log.UseConsoleOutput(); + Log.Trace("~"); + Goshujin.Init(); + Goshujin.LoadCmdTable(); + Goshujin.LoadMethodTable(); + Goshujin.ResumeMainThread(); + // + Native.WaitMainWindow(); + Log.ResetConsole(); + // + RecordChecksum(); + MinHook.Attach(GameMethod.DoCmd, &OnDoCmd, out _doCmd); + MinHook.Attach(GameMethod.ToUInt16, &OnToUInt16, out _toUInt16); + MinHook.Attach(GameMethod.UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp); + return 0; + } + + #region RecvPacket + + private static delegate*unmanaged _toUInt16; + + [UnmanagedCallersOnly] + private static ushort OnToUInt16(byte* val, int startIndex) { + var ret = _toUInt16(val, startIndex); + if (ret != 0xAB89 || *(ushort*) (val += 0x20) != 0x6745) { + return ret; + } + var cmdId = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 2)); + if (cmdId == CmdId.PlayerStoreNotify) { + Goshujin.PushStoreData(GetData(val)); + } else if (cmdId == CmdId.AchievementAllDataNotify) { + Goshujin.PushAchievementData(GetData(val)); + } + return ret; + static Span GetData(byte* val) { + var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4)); + var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6)); + return new Span(val + 10 + headLen, (int) dataLen); + } + } + + #endregion + + #region Prop + + /* + * PROP_PLAYER_HCOIN = 10015, + * PROP_PLAYER_WAIT_SUB_HCOIN = 10022, + * PROP_PLAYER_SCOIN = 10016, + * PROP_PLAYER_WAIT_SUB_SCOIN = 10023, + * PROP_PLAYER_MCOIN = 10025, + * PROP_PLAYER_WAIT_SUB_MCOIN = 10026, + * PROP_PLAYER_HOME_COIN = 10042, + * PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043, + * PROP_PLAYER_ROLE_COMBAT_COIN = 10053, + * PROP_PLAYER_MUSIC_GAME_BOOK_COIN = 10058, + */ + public static HashSet RequiredPlayerProperties { get; } = [ + 10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058 + ]; + + private static delegate*unmanaged _updateNormalProp; + + [UnmanagedCallersOnly] + private static void OnUpdateNormalProp(nint @this, int type, double value, double lastValue, int state) { + _updateNormalProp(@this, type, value, lastValue, state); + if (RequiredPlayerProperties.Remove(type)) { + Goshujin.PushPlayerProp(type, value); + } + } + + #endregion + + #region Checksum + + [StructLayout(LayoutKind.Sequential)] + private struct RecordChecksumCmdData { + + public int Type; + + public void* Buffer; + + public int Length; + + } + + private static readonly RecordChecksumCmdData[] RecordedChecksum = new RecordChecksumCmdData[3]; + + private static void RecordChecksum() { + for (var i = 0; i < 3; i++) { + var buffer = NativeMemory.AllocZeroed(256); + var data = new RecordChecksumCmdData { + Type = i, + Buffer = buffer, + Length = 256 + }; + _ = GameMethod.DoCmd(23, Unsafe.AsPointer(ref data), sizeof(RecordChecksumCmdData)); + RecordedChecksum[i] = data; + //REPL//Log.Trace($"nType={i}, value={new string((sbyte*) buffer, 0, data.Length)}"); + } + } + + private static delegate*unmanaged _doCmd; + + [UnmanagedCallersOnly] + public static int OnDoCmd(int cmdType, void* data, int size) { + var result = _doCmd(cmdType, data, size); + if (cmdType == 23) { + var cmdData = (RecordChecksumCmdData*) data; + if (cmdData->Type < 3) { + var recordedData = RecordedChecksum[cmdData->Type]; + cmdData->Length = recordedData.Length; + Buffer.MemoryCopy(recordedData.Buffer, cmdData->Buffer, recordedData.Length, recordedData.Length); + //REPL//Log.Trace($"Override type {cmdData->Type} result"); + } + } + return result; + } + + #endregion + +} diff --git a/YaeAchievementLib/src/Goshujin.cs b/YaeAchievementLib/src/Goshujin.cs new file mode 100644 index 0000000..232b430 --- /dev/null +++ b/YaeAchievementLib/src/Goshujin.cs @@ -0,0 +1,88 @@ +using System.IO.Pipes; +using Yae.Utilities; + +namespace Yae; + +internal static class CmdId { + + public static uint AchievementAllDataNotify { get; set; } + + public static uint PlayerStoreNotify { get; set; } + +} + +internal static unsafe class GameMethod { + + public static delegate*unmanaged DoCmd { get; set; } + + public static delegate*unmanaged ToUInt16 { get; set; } + + public static delegate*unmanaged UpdateNormalProp { get; set; } + +} + +internal static class Goshujin { + + private static NamedPipeClientStream _pipeStream = null!; + private static BinaryReader _pipeReader = null!; + private static BinaryWriter _pipeWriter = null!; + + public static void Init(string pipeName = "YaeAchievementPipe") { + _pipeStream = new NamedPipeClientStream(pipeName); + _pipeReader = new BinaryReader(_pipeStream); + _pipeWriter = new BinaryWriter(_pipeStream); + _pipeStream.Connect(); + Log.Trace("Pipe server connected."); + } + + public static void PushAchievementData(Span data) { + _pipeWriter.Write((byte) 1); + _pipeWriter.Write(data.Length); + _pipeWriter.Write(data); + _achievementDataPushed = true; + ExitIfFinished(); + } + + public static void PushStoreData(Span data) { + _pipeWriter.Write((byte) 2); + _pipeWriter.Write(data.Length); + _pipeWriter.Write(data); + _storeDataPushed = true; + ExitIfFinished(); + } + + public static void PushPlayerProp(int type, double value) { + _pipeWriter.Write((byte) 3); + _pipeWriter.Write(type); + _pipeWriter.Write(value); + ExitIfFinished(); + } + + public static void LoadCmdTable() { + _pipeWriter.Write((byte) 0xFC); + CmdId.AchievementAllDataNotify = _pipeReader.ReadUInt32(); + CmdId.PlayerStoreNotify = _pipeReader.ReadUInt32(); + } + + public static unsafe void LoadMethodTable() { + _pipeWriter.Write((byte) 0xFD); + GameMethod.DoCmd = (delegate*unmanaged) Native.RVAToVA(_pipeReader.ReadUInt32()); + GameMethod.ToUInt16 = (delegate*unmanaged) Native.RVAToVA(_pipeReader.ReadUInt32()); + GameMethod.UpdateNormalProp = (delegate*unmanaged) Native.RVAToVA(_pipeReader.ReadUInt32()); + } + + public static void ResumeMainThread() { + _pipeWriter.Write((byte) 0xFE); + } + + private static bool _storeDataPushed; + + private static bool _achievementDataPushed; + + private static void ExitIfFinished() { + if (_storeDataPushed && _achievementDataPushed && Application.RequiredPlayerProperties.Count == 0) { + _pipeWriter.Write((byte) 0xFF); + Environment.Exit(0); + } + } +} diff --git a/YaeAchievementLib/src/Utilities/Log.cs b/YaeAchievementLib/src/Utilities/Log.cs new file mode 100644 index 0000000..0fbff66 --- /dev/null +++ b/YaeAchievementLib/src/Utilities/Log.cs @@ -0,0 +1,111 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Yae.Utilities; + +[Flags] +internal enum LogLevel : byte { + Trace = 0x00, + Debug = 0x01, + Info = 0x02, + Warn = 0x03, + Error = 0x04, + Fatal = 0x05, + Time = 0x06, + LevelMask = 0x0F, + FileOnly = 0x10, +} + +internal static class Log { + + #region ConsoleWriter + + private static TextWriter? _consoleWriter; + + [Conditional("EnableLogging")] + public static void UseConsoleOutput() { + InitializeConsole(); + _consoleWriter = Console.Out; + } + + [Conditional("EnableLogging")] + public static void ResetConsole() { + Kernel32.FreeConsole(); + InitializeConsole(); + var sw = new StreamWriter(Console.OpenStandardOutput(), _consoleWriter!.Encoding, 256, true) { + AutoFlush = true + }; + _consoleWriter = TextWriter.Synchronized(sw); + Console.SetOut(_consoleWriter); + } + + private static unsafe void InitializeConsole() { + Kernel32.AllocConsole(); + uint mode; + var cHandle = Kernel32.GetStdHandle(Kernel32.STD_OUTPUT_HANDLE); + if (!Kernel32.GetConsoleMode(cHandle, &mode)) { + return; + } + Kernel32.SetConsoleMode(cHandle, mode | Kernel32.ENABLE_VIRTUAL_TERMINAL_PROCESSING); + Console.OutputEncoding = Console.InputEncoding = System.Text.Encoding.UTF8; + } + + #endregion + + [DoesNotReturn] + public static void ErrorAndExit(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Fatal); + Environment.Exit(0); + } + + public static void Error(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Error); + } + + public static void Warn(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Warn); + } + + public static void Info(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Info); + } + + public static void Debug(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Debug); + } + + public static void Trace(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Trace); + } + + public static void Time(string value, [CallerMemberName] string callerName = "") { + WriteLog(value, callerName, LogLevel.Time); + } + + [Conditional("EnableLogging")] + public static void WriteLog(string message, string tag, LogLevel level) { + var time = DateTimeOffset.Now.ToString("HH:mm:ss.fff"); + if (_consoleWriter != null) { + var color = level switch { + LogLevel.Error or LogLevel.Fatal => "244;67;54", + LogLevel.Warn => "255;235;59", + LogLevel.Info => "153;255;153", + LogLevel.Debug => "91;206;250", + LogLevel.Trace => "246;168;184", + LogLevel.Time => "19;161;14", + _ => throw new ArgumentException($"Invalid log level: {level}") + }; + _consoleWriter.Write($"[{time}][\e[38;2;{color}m{level,5}\e[0m] {tag} : "); + _consoleWriter.WriteLine(message); + } + if (level == LogLevel.Fatal) { + if (_consoleWriter != null) { + WriteLog("Error occurred, press enter key to exit", tag, LogLevel.Error); + Console.ReadLine(); + } else { + User32.MessageBoxW(0, "An critical error occurred.", "Error", 0x10); + } + } + } +} diff --git a/YaeAchievementLib/src/Utilities/Native.cs b/YaeAchievementLib/src/Utilities/Native.cs new file mode 100644 index 0000000..acbbde7 --- /dev/null +++ b/YaeAchievementLib/src/Utilities/Native.cs @@ -0,0 +1,201 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Yae.Utilities; + +internal static unsafe class Native { + + #region WaitMainWindow + + private static nint _hwnd; + private static readonly uint ProcessId = Kernel32.GetCurrentProcessId(); + + public static void WaitMainWindow() { + _hwnd = 0; + do { + Thread.Sleep(100); + _ = User32.EnumWindows(&EnumWindowsCallback, 0); + } while (_hwnd == 0); + return; + [UnmanagedCallersOnly(CallConvs = [ typeof(CallConvStdcall) ])] + static int EnumWindowsCallback(nint handle, nint extraParameter) { + uint wProcessId = 0; // Avoid uninitialized variable if the window got closed in the meantime + _ = User32.GetWindowThreadProcessId(handle, &wProcessId); + var cName = (char*) NativeMemory.Alloc(256); + if (User32.GetClassNameW(handle, cName, 256) != 0) { + if (wProcessId == ProcessId && User32.IsWindowVisible(handle) && new string(cName) == "UnityWndClass") { + _hwnd = handle; + } + } + NativeMemory.Free(cName); + return _hwnd == 0 ? 1 : 0; + } + } + + #endregion + + #region RestoreVirtualProtect + + public static bool RestoreVirtualProtect() { + // NtProtectVirtualMemoryImpl + // _ = stackalloc byte[] { 0x4C, 0x8B, 0xD1, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x05, 0xC3 }; + if (!NativeLibrary.TryLoad("ntdll.dll", out var hPtr)) { + return false; + } + if (!NativeLibrary.TryGetExport(hPtr, "NtProtectVirtualMemory", out var mPtr)) { + return false; + } + // 4C 8B D1 mov r10, rcx + // B8 mov eax, $imm32 + if (*(uint*) (mPtr - 0x20) != 0xB8D18B4C) { // previous + return false; + } + var syscallNumber = (ulong) *(uint*) (mPtr - 0x1C) + 1; + var old = 0u; + if (!Kernel32.VirtualProtect(mPtr, 1, Kernel32.PAGE_EXECUTE_READWRITE, &old)) { + return false; + } + *(ulong*) mPtr = 0xB8D18B4C | syscallNumber << 32; + return Kernel32.VirtualProtect(mPtr, 1, old, &old); + } + + #endregion + + #region GetModuleHandle + + public static string GetModulePath(nint hModule) { + var buffer = stackalloc char[256]; + _ = Kernel32.GetModuleFileNameW(hModule, buffer, 256); + return new string(buffer); + } + + public static nint GetModuleHandle(string? moduleName = null) { + fixed (char* pName = moduleName ?? Path.GetFileName(GetModulePath(0))) { + return Kernel32.GetModuleHandleW(pName); + } + } + + #endregion + + private static readonly nint ModuleBase = GetModuleHandle(); + + public static nint RVAToVA(uint addr) => ModuleBase + (nint) addr; + + public static void RegisterUnhandledExceptionHandler() { + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + return; + static void OnUnhandledException(object? sender, UnhandledExceptionEventArgs e) { + var ex = e.ExceptionObject as Exception; + User32.MessageBoxW(0, ex?.ToString() ?? "null", "Unhandled Exception", 0x10); + Environment.Exit(-1); + } + } + +} + +internal static partial class MinHook { + + /// + /// Initialize the MinHook library. You must call this function EXACTLY ONCE at the beginning of your program. + /// + [LibraryImport("libMinHook.x64", EntryPoint = "MH_Initialize")] + private static partial uint MinHookInitialize(); + + /// + /// Creates a hook for the specified target function, in disabled state. + /// + /// A pointer to the target function, which will be overridden by the detour function. + /// A pointer to the detour function, which will override the target function. + /// + /// A pointer to the trampoline function, which will be used to call the original target function. + /// This parameter can be NULL. + /// + [LibraryImport("libMinHook.x64", EntryPoint = "MH_CreateHook")] + private static partial uint MinHookCreate(nint pTarget, nint pDetour, out nint ppOriginal); + + /// + /// Enables an already created hook. + /// + /// + /// A pointer to the target function. + /// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go. + /// + [LibraryImport("libMinHook.x64", EntryPoint = "MH_EnableHook")] + private static partial uint MinHookEnable(nint pTarget); + + /// + /// Disables an already created hook. + /// + /// + /// A pointer to the target function. + /// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go. + /// + [LibraryImport("libMinHook.x64", EntryPoint = "MH_DisableHook")] + private static partial uint MinHookDisable(nint pTarget); + + /// + /// Removes an already created hook. + /// + /// A pointer to the target function. + [LibraryImport("libMinHook.x64", EntryPoint = "MH_RemoveHook")] + private static partial uint MinHookRemove(nint pTarget); + + /// + /// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program. + /// + [LibraryImport("libMinHook.x64", EntryPoint = "MH_Uninitialize")] + private static partial uint MinHookUninitialize(); + + static MinHook() { + var result = MinHookInitialize(); + if (result != 0) { + throw new InvalidOperationException($"Failed to initialize MinHook: {result}"); + } + } + + // todo: auto gen + public static unsafe void Attach(delegate*unmanaged origin, delegate*unmanaged handler, out delegate*unmanaged trampoline) { + Attach((nint) origin, (nint) handler, out var trampoline1); + trampoline = (delegate*unmanaged) trampoline1; + } + + // todo: auto gen + public static unsafe void Attach(delegate*unmanaged origin, delegate*unmanaged handler, out delegate*unmanaged trampoline) { + Attach((nint) origin, (nint) handler, out var trampoline1); + trampoline = (delegate*unmanaged) trampoline1; + } + + // todo: auto gen + public static unsafe void Attach(delegate*unmanaged origin, delegate*unmanaged handler, out delegate*unmanaged trampoline) { + Attach((nint) origin, (nint) handler, out var trampoline1); + trampoline = (delegate*unmanaged) trampoline1; + } + + // todo: auto gen + public static unsafe void Attach(delegate*unmanaged origin, delegate*unmanaged handler, out delegate*unmanaged trampoline) { + Attach((nint) origin, (nint) handler, out var trampoline1); + trampoline = (delegate*unmanaged) trampoline1; + } + + public static void Attach(nint origin, nint handler, out nint trampoline) { + uint result; + if ((result = MinHookCreate(origin, handler, out trampoline)) != 0) { + throw new InvalidOperationException($"Failed to create hook: {result}"); + } + if ((result = MinHookEnable(origin)) != 0) { + throw new InvalidOperationException($"Failed to enable hook: {result}"); + } + } + + public static void Detach(nint origin) { + uint result; + if ((result = MinHookDisable(origin)) != 0) { + throw new InvalidOperationException($"Failed to create hook: {result}"); + } + if ((result = MinHookRemove(origin)) != 0) { + throw new InvalidOperationException($"Failed to enable hook: {result}"); + } + } +} diff --git a/YaeAchievementLib/src/Utilities/WinApi.cs b/YaeAchievementLib/src/Utilities/WinApi.cs new file mode 100644 index 0000000..23c1e76 --- /dev/null +++ b/YaeAchievementLib/src/Utilities/WinApi.cs @@ -0,0 +1,68 @@ +using System.Runtime.InteropServices; + +namespace Yae.Utilities; + +#pragma warning disable CS0649, CA1069 // ReSharper disable IdentifierTypo, InconsistentNaming, UnassignedField.Global + +internal static unsafe partial class Kernel32 { + + [LibraryImport("KERNEL32.dll")] + internal static partial uint GetCurrentProcessId(); + + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial nint GetModuleHandleW(char* lpModuleName); + + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial uint GetModuleFileNameW(nint hModule, char* lpFilename, uint nSize); + + internal const uint PAGE_EXECUTE_READWRITE = 0x00000040; + + [return:MarshalAs(UnmanagedType.I4)] + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, uint* lpflOldProtect); + + internal const uint STD_OUTPUT_HANDLE = 0xFFFFFFF5; + + internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x00000004; + + [return:MarshalAs(UnmanagedType.I4)] + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial bool AllocConsole(); + + [return:MarshalAs(UnmanagedType.I4)] + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial bool FreeConsole(); + + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial nint GetStdHandle(uint nStdHandle); + + [return:MarshalAs(UnmanagedType.I4)] + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial bool GetConsoleMode(nint hConsoleHandle, uint* lpMode); + + [return:MarshalAs(UnmanagedType.I4)] + [LibraryImport("KERNEL32.dll", SetLastError = true)] + internal static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode); + +} + +internal static unsafe partial class User32 { + + [LibraryImport("USER32.dll", SetLastError = true)] + internal static partial uint GetWindowThreadProcessId(nint hWnd, uint* lpdwProcessId); + + [LibraryImport("USER32.dll", SetLastError = true)] + internal static partial int GetClassNameW(nint hWnd, char* lpClassName, int nMaxCount); + + [return: MarshalAs(UnmanagedType.I4)] + [LibraryImport("USER32.dll")] + internal static partial bool IsWindowVisible(nint hWnd); + + [return: MarshalAs(UnmanagedType.I4)] + [LibraryImport("USER32.dll", SetLastError = true)] + internal static partial bool EnumWindows(delegate *unmanaged[Stdcall] lpEnumFunc, nint lParam); + + [LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType); + +}