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