refactor native lib

This commit is contained in:
HolographicHat
2025-08-02 01:58:01 +08:00
parent 1216ffb872
commit 7fc296f6e9
17 changed files with 742 additions and 25 deletions

View File

@@ -1,3 +1,4 @@
<Solution> <Solution>
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" Type="Classic C#" />
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" /> <Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" />
</Solution> </Solution>

View File

@@ -222,7 +222,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// 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}).
/// </summary> /// </summary>
internal static string ExportToCocogoatFail { internal static string ExportToCocogoatFail {
get { get {

View File

@@ -19,7 +19,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ExportToCocogoatFail" xml:space="preserve"> <data name="ExportToCocogoatFail" xml:space="preserve">
<value>Fail, please contact developer to get help information</value> <value>Fail, please contact developer to get help information (CG_{0})</value>
</data> </data>
<data name="AllAchievement" xml:space="preserve"> <data name="AllAchievement" xml:space="preserve">
<value>all achievement</value> <value>all achievement</value>

View File

@@ -12,7 +12,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ExportToCocogoatFail" xml:space="preserve"> <data name="ExportToCocogoatFail" xml:space="preserve">
<value>导出失败, 请联系开发者以获取帮助</value> <value>导出失败, 请联系开发者以获取帮助CG_{0}</value>
</data> </data>
<data name="AllAchievement" xml:space="preserve"> <data name="AllAchievement" xml:space="preserve">
<value>全部成就</value> <value>全部成就</value>

View File

@@ -17,9 +17,22 @@ message AchievementItem {
string description = 4; 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<uint32, MethodRvaConfig> method_rva = 10;
}
message AchievementInfo { message AchievementInfo {
string version = 1; string version = 1;
map<uint32, string> group = 2; map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3; map<uint32, AchievementItem> items = 3;
AchievementProtoFieldInfo pb_info = 4; AchievementProtoFieldInfo pb_info = 4;
NativeLibConfig native_config = 5;
} }

View File

@@ -10,6 +10,4 @@ message UpdateInfo {
bool force_update = 5; bool force_update = 5;
bool enable_lib_download = 6; bool enable_lib_download = 6;
bool enable_auto_update = 7; bool enable_auto_update = 7;
string current_cn_hash = 8;
string current_os_hash = 9;
} }

View File

@@ -48,7 +48,7 @@ public static class Export {
request.Content = new StringContent(result, Encoding.UTF8, "application/json"); request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request); using var response = Utils.CHttpClient.Send(request);
if (response.StatusCode != HttpStatusCode.Created) { if (response.StatusCode != HttpStatusCode.Created) {
AnsiConsole.WriteLine(App.ExportToCocogoatFail); AnsiConsole.WriteLine(App.ExportToCocogoatFail, response.StatusCode);
return; return;
} }
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();

View File

@@ -59,7 +59,7 @@ internal static class Program {
} }
} catch (Exception) { /* ignored */ } } 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<string>() var prompt = new SelectionPromptCompat<string>()
.Title(App.UsePreviousData) .Title(App.UsePreviousData)
.AddChoices(App.CommonYes, App.CommonNo); .AddChoices(App.CommonYes, App.CommonNo);
@@ -72,7 +72,7 @@ internal static class Program {
StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> { StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> {
{ 1, AchievementAllDataNotify.OnReceive }, { 1, AchievementAllDataNotify.OnReceive },
{ 2, PlayerStoreNotify.OnReceive }, { 2, PlayerStoreNotify.OnReceive },
{ 100, PlayerPropNotify.OnReceive }, { 3, PlayerPropNotify.OnReceive },
}, () => { }, () => {
#if DEBUG_EX #if DEBUG_EX
PlayerPropNotify.OnFinish(); PlayerPropNotify.OnFinish();

View File

@@ -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<byte> buf) {
var checksum = 0xFFFFFFFF;
foreach (var b in buf) {
checksum = (checksum >> 8) ^ Crc32Table[(b ^ checksum) & 0xFF];
}
return ~checksum;
}
}

View File

@@ -203,30 +203,40 @@ public static class Utils {
} }
private static bool _isUnexpectedExit = true; private static bool _isUnexpectedExit = true;
// ReSharper disable once UnusedMethodReturnValue.Global // ReSharper disable once UnusedMethodReturnValue.Global
public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> handlers, Action onFinish) { public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> handlers, Action onFinish) {
_proc = new GameProcess(exePath); var hash = GetGameHash(exePath);
_proc.LoadLibrary(GlobalVars.LibFilePath); var nativeConf = GlobalVars.AchievementInfo.NativeConfig;
_proc.ResumeMainThread(); if (!nativeConf.MethodRva.TryGetValue(hash, out var methodRva)) {
_proc.OnExit += () => { AnsiConsole.WriteLine($"No match config {exePath} {hash:X8}");
if (_isUnexpectedExit) { Environment.Exit(0);
_proc = null; return;
AnsiConsole.WriteLine(App.GameProcessExit); }
Environment.Exit(114514);
}
};
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
Task.Run(() => { Task.Run(() => {
using var stream = new NamedPipeServerStream(GlobalVars.PipeName); 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(); stream.WaitForConnection();
int type; int type;
while ((type = stream.ReadByte()) != -1) { while ((type = stream.ReadByte()) != -1) {
if (type == 0xFF) { switch (type) {
_isUnexpectedExit = false; case 0xFC:
onFinish(); writer.Write(nativeConf.AchievementCmdId);
break; 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 (handlers.TryGetValue(type, out var handler)) {
if (handler(reader)) { if (handler(reader)) {
@@ -235,6 +245,23 @@ public static class Utils {
} }
} }
}).ContinueWith(task => { if (task.IsFaulted) OnUnhandledException(task.Exception!); }); }).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<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(exePath);
_ = stream.Read(buffer);
return Crc32.Compute(buffer);
} }
internal static unsafe void SetQuickEditMode(bool enable) { internal static unsafe void SetQuickEditMode(bool enable) {

View File

@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<RootNamespace>Yae</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0-windows</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<InvariantGlobalization>true</InvariantGlobalization>
<OptimizationPreference>Speed</OptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elysia.Bootstrap" Version="1.0.14"/>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<DirectPInvoke Include="NTDLL"/>
<DirectPInvoke Include="USER32"/>
<DirectPInvoke Include="KERNEL32"/>
<DirectPInvoke Include="libMinHook.x64"/>
<NativeLibrary Include="lib\libMinHook.x64.lib"/>
</ItemGroup>
<ItemGroup>
<LibraryEntrypoint Include="YaeMain"/>
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute"/>
</ItemGroup>
<ItemGroup>
<Using Include="System.Diagnostics"/>
<Using Include="System.Diagnostics.CodeAnalysis"/>
</ItemGroup>
<ItemGroup>
<Folder Include="obj\" />
</ItemGroup>
</Project>

Binary file not shown.

View File

@@ -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<byte*, int, ushort> _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<byte> GetData(byte* val) {
var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4));
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
return new Span<byte>(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<int> RequiredPlayerProperties { get; } = [
10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058
];
private static delegate*unmanaged<nint, int, double, double, int, void> _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<int, void*, int, int> _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
}

View File

@@ -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<int, void*, int, int> DoCmd { get; set; }
public static delegate*unmanaged<byte*, int, ushort> ToUInt16 { get; set; }
public static delegate*unmanaged<nint, int, double, double, int, void> 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<byte> data) {
_pipeWriter.Write((byte) 1);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_achievementDataPushed = true;
ExitIfFinished();
}
public static void PushStoreData(Span<byte> 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<int, void*, int, int>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.ToUInt16 = (delegate*unmanaged<byte*, int, ushort>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.UpdateNormalProp = (delegate*unmanaged<nint, int, double, double, int, void>) 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);
}
}
}

View File

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

View File

@@ -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 {
/// <summary>
/// Initialize the MinHook library. You must call this function EXACTLY ONCE at the beginning of your program.
/// </summary>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Initialize")]
private static partial uint MinHookInitialize();
/// <summary>
/// Creates a hook for the specified target function, in disabled state.
/// </summary>
/// <param name="pTarget">A pointer to the target function, which will be overridden by the detour function.</param>
/// <param name="pDetour">A pointer to the detour function, which will override the target function.</param>
/// <param name="ppOriginal">
/// A pointer to the trampoline function, which will be used to call the original target function.
/// This parameter can be NULL.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_CreateHook")]
private static partial uint MinHookCreate(nint pTarget, nint pDetour, out nint ppOriginal);
/// <summary>
/// Enables an already created hook.
/// </summary>
/// <param name="pTarget">
/// A pointer to the target function.
/// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_EnableHook")]
private static partial uint MinHookEnable(nint pTarget);
/// <summary>
/// Disables an already created hook.
/// </summary>
/// <param name="pTarget">
/// A pointer to the target function.
/// If this parameter is MH_ALL_HOOKS, all created hooks are enabled in one go.
/// </param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_DisableHook")]
private static partial uint MinHookDisable(nint pTarget);
/// <summary>
/// Removes an already created hook.
/// </summary>
/// <param name="pTarget">A pointer to the target function.</param>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_RemoveHook")]
private static partial uint MinHookRemove(nint pTarget);
/// <summary>
/// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program.
/// </summary>
[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<byte*, int, ushort> origin, delegate*unmanaged<byte*, int, ushort> handler, out delegate*unmanaged<byte*, int, ushort> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<byte*, int, ushort>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<nint, int, double, double, int, void> origin, delegate*unmanaged<nint, int, double, double, int, void> handler, out delegate*unmanaged<nint, int, double, double, int, void> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<nint, int, double, double, int, void>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<nint, nint, uint, void> origin, delegate*unmanaged<nint, nint, uint, void> handler, out delegate*unmanaged<nint, nint, uint, void> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<nint, nint, uint, void>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<int, void*, int, int> origin, delegate*unmanaged<int, void*, int, int> handler, out delegate*unmanaged<int, void*, int, int> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<int, void*, int, int>) 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}");
}
}
}

View File

@@ -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]<nint, nint, int> lpEnumFunc, nint lParam);
[LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType);
}