mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-13 18:08:15 +08:00
refactor native lib
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" Type="Classic C#" />
|
||||
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" />
|
||||
</Solution>
|
||||
2
YaeAchievement/res/App.Designer.cs
generated
2
YaeAchievement/res/App.Designer.cs
generated
@@ -222,7 +222,7 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static string ExportToCocogoatFail {
|
||||
get {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<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 name="AllAchievement" xml:space="preserve">
|
||||
<value>all achievement</value>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ExportToCocogoatFail" xml:space="preserve">
|
||||
<value>导出失败, 请联系开发者以获取帮助</value>
|
||||
<value>导出失败, 请联系开发者以获取帮助(CG_{0})</value>
|
||||
</data>
|
||||
<data name="AllAchievement" xml:space="preserve">
|
||||
<value>全部成就</value>
|
||||
|
||||
@@ -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<uint32, MethodRvaConfig> method_rva = 10;
|
||||
}
|
||||
|
||||
message AchievementInfo {
|
||||
string version = 1;
|
||||
map<uint32, string> group = 2;
|
||||
map<uint32, AchievementItem> items = 3;
|
||||
AchievementProtoFieldInfo pb_info = 4;
|
||||
NativeLibConfig native_config = 5;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string>()
|
||||
.Title(App.UsePreviousData)
|
||||
.AddChoices(App.CommonYes, App.CommonNo);
|
||||
@@ -72,7 +72,7 @@ internal static class Program {
|
||||
StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> {
|
||||
{ 1, AchievementAllDataNotify.OnReceive },
|
||||
{ 2, PlayerStoreNotify.OnReceive },
|
||||
{ 100, PlayerPropNotify.OnReceive },
|
||||
{ 3, PlayerPropNotify.OnReceive },
|
||||
}, () => {
|
||||
#if DEBUG_EX
|
||||
PlayerPropNotify.OnFinish();
|
||||
|
||||
26
YaeAchievement/src/Utilities/Crc32.cs
Normal file
26
YaeAchievement/src/Utilities/Crc32.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -206,27 +206,37 @@ public static class Utils {
|
||||
|
||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||
public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> 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<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) {
|
||||
|
||||
50
YaeAchievementLib/YaeAchievementLib.csproj
Normal file
50
YaeAchievementLib/YaeAchievementLib.csproj
Normal 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>
|
||||
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
Binary file not shown.
134
YaeAchievementLib/src/Application.cs
Normal file
134
YaeAchievementLib/src/Application.cs
Normal 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
|
||||
|
||||
}
|
||||
88
YaeAchievementLib/src/Goshujin.cs
Normal file
88
YaeAchievementLib/src/Goshujin.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
YaeAchievementLib/src/Utilities/Log.cs
Normal file
111
YaeAchievementLib/src/Utilities/Log.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
YaeAchievementLib/src/Utilities/Native.cs
Normal file
201
YaeAchievementLib/src/Utilities/Native.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
68
YaeAchievementLib/src/Utilities/WinApi.cs
Normal file
68
YaeAchievementLib/src/Utilities/WinApi.cs
Normal 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);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user