mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-11 00:48:12 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccff7a1702 | ||
|
|
79122d6ba7 | ||
|
|
1218d1cbc4 | ||
|
|
19f84bdb86 | ||
|
|
9d22fdf6f9 | ||
|
|
27fa0d9c84 | ||
|
|
e9baf8f211 | ||
|
|
b960165b7e | ||
|
|
a4c2027ada | ||
|
|
f49477c49a | ||
|
|
67c2fb3bda | ||
|
|
be3440695d | ||
|
|
f8b8a5a9e1 | ||
|
|
49f8679996 | ||
|
|
222a26233e | ||
|
|
1130f442fc | ||
|
|
b80987f574 | ||
|
|
c96395e1a2 | ||
|
|
3f42156b20 | ||
|
|
45638b7327 | ||
|
|
d514f3b5e7 | ||
|
|
3fe54d908e |
12
.github/workflows/dotnet.yml
vendored
12
.github/workflows/dotnet.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: .NET Build
|
name: .NET Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -13,7 +14,8 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./YaeAchievement
|
working-directory: ./YaeAchievement
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
@@ -27,12 +29,12 @@ jobs:
|
|||||||
- name: Upload-AOT
|
- name: Upload-AOT
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Artifacts-AOT
|
name: aot
|
||||||
path: publish\publish
|
path: YaeAchievement\publish\publish
|
||||||
- name: Publish-NoAOT
|
- name: Publish-NoAOT
|
||||||
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
|
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
|
||||||
- name: Upload-NoAOT
|
- name: Upload-NoAOT
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Artifacts-NoAOT
|
name: normal
|
||||||
path: naot-publish\publish
|
path: YaeAchievement\naot-publish\publish
|
||||||
|
|||||||
46
.github/workflows/lib-nuget.yml
vendored
46
.github/workflows/lib-nuget.yml
vendored
@@ -2,29 +2,35 @@ name: YaeLib NuGet Publish
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
inputs:
|
||||||
types: [released]
|
confirm_version:
|
||||||
|
description: 'Version already increased?'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
perform_publish:
|
||||||
|
description: 'Publish to nuget?'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: YaeAchievementLib
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
- name: Setup MSBuild
|
dotnet-version: 9.0.x
|
||||||
uses: microsoft/setup-msbuild@v2
|
- name: Build native library
|
||||||
|
run: dotnet publish
|
||||||
- name: Restore NuGet Packages
|
|
||||||
run: nuget restore lib\YaeAchievementLib.sln
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
continue-on-error: true
|
|
||||||
run: msbuild lib\YaeAchievementLib.sln /p:Configuration=Release
|
|
||||||
|
|
||||||
- name: Pack
|
|
||||||
run: nuget pack lib\YaeAchievementLib.nuspec
|
|
||||||
|
|
||||||
- name: Publish to NuGet
|
- name: Publish to NuGet
|
||||||
run: nuget push *.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
|
if: ${{ inputs.perform_publish }}
|
||||||
|
run: nuget push bin\Release\*.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
|
||||||
|
- name: Upload nuget package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nupkg
|
||||||
|
path: YaeAchievementLib\bin\Release\*.nupkg
|
||||||
|
|||||||
104
YaeAchievement.SourceGeneration/MinHookAttachGenerator.cs
Normal file
104
YaeAchievement.SourceGeneration/MinHookAttachGenerator.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Threading;
|
||||||
|
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||||
|
|
||||||
|
namespace YaeAchievement.SourceGeneration;
|
||||||
|
|
||||||
|
[Generator(LanguageNames.CSharp)]
|
||||||
|
public sealed class MinHookAttachGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
IncrementalValueProvider<ImmutableArray<AttachInfo>> provider = context.SyntaxProvider.CreateSyntaxProvider(Filter, Transform).Collect();
|
||||||
|
context.RegisterSourceOutput(provider, Generate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Filter(SyntaxNode node, CancellationToken token)
|
||||||
|
{
|
||||||
|
return node is InvocationExpressionSyntax
|
||||||
|
{
|
||||||
|
Expression: MemberAccessExpressionSyntax
|
||||||
|
{
|
||||||
|
Expression: IdentifierNameSyntax { Identifier.Text: "MinHook" },
|
||||||
|
Name.Identifier.Text: "Attach"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttachInfo Transform(GeneratorSyntaxContext context, CancellationToken token)
|
||||||
|
{
|
||||||
|
InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node;
|
||||||
|
SeparatedSyntaxList<ArgumentSyntax> args = invocation.ArgumentList.Arguments;
|
||||||
|
if (args.Count is not 3)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string type = context.SemanticModel.GetTypeInfo(args[0].Expression).Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(type))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
MinimallyQualifiedType = type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Generate(SourceProductionContext context, ImmutableArray<AttachInfo> infoArray)
|
||||||
|
{
|
||||||
|
CompilationUnitSyntax unit = CompilationUnit()
|
||||||
|
.WithMembers(List<MemberDeclarationSyntax>(
|
||||||
|
[
|
||||||
|
FileScopedNamespaceDeclaration(ParseName("Yae.Utilities")),
|
||||||
|
ClassDeclaration("MinHook")
|
||||||
|
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)))
|
||||||
|
.WithMembers(List(GenerateMethods(infoArray)))
|
||||||
|
]));
|
||||||
|
|
||||||
|
context.AddSource("MinHook.Attach.g.cs", unit.NormalizeWhitespace().ToFullString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<MemberDeclarationSyntax> GenerateMethods(ImmutableArray<AttachInfo> infoArray)
|
||||||
|
{
|
||||||
|
foreach (AttachInfo info in infoArray)
|
||||||
|
{
|
||||||
|
TypeSyntax type = ParseTypeName(info.MinimallyQualifiedType);
|
||||||
|
|
||||||
|
yield return MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("Attach"))
|
||||||
|
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.UnsafeKeyword)))
|
||||||
|
.WithParameterList(ParameterList(SeparatedList(
|
||||||
|
[
|
||||||
|
Parameter(Identifier("origin")).WithType(type),
|
||||||
|
Parameter(Identifier("handler")).WithType(type),
|
||||||
|
Parameter(Identifier("trampoline")).WithType(type).WithModifiers(TokenList(Token(SyntaxKind.OutKeyword)))
|
||||||
|
])))
|
||||||
|
.WithBody(Block(List<StatementSyntax>(
|
||||||
|
[
|
||||||
|
ExpressionStatement(InvocationExpression(IdentifierName("Attach"))
|
||||||
|
.WithArgumentList(ArgumentList(SeparatedList(
|
||||||
|
[
|
||||||
|
Argument(CastExpression(IdentifierName("nint"), IdentifierName("origin"))),
|
||||||
|
Argument(CastExpression(IdentifierName("nint"), IdentifierName("handler"))),
|
||||||
|
Argument(DeclarationExpression(IdentifierName("nint"), SingleVariableDesignation(Identifier("trampoline1"))))
|
||||||
|
.WithRefKindKeyword(Token(SyntaxKind.OutKeyword))
|
||||||
|
])))),
|
||||||
|
ExpressionStatement(AssignmentExpression(
|
||||||
|
SyntaxKind.SimpleAssignmentExpression,
|
||||||
|
IdentifierName("trampoline"),
|
||||||
|
CastExpression(type, IdentifierName("trampoline1"))))
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AttachInfo
|
||||||
|
{
|
||||||
|
public required string MinimallyQualifiedType { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||||
|
<PackageReference Include="PolySharp" Version="1.15.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" Type="Classic C#" />
|
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
|
||||||
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" />
|
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
|
||||||
|
<Project Path="YaeAchievement\YaeAchievement.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||||
"className": "Native",
|
"className": "Native",
|
||||||
"allowMarshaling": false,
|
"allowMarshaling": false,
|
||||||
"public": true
|
"public": false
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,30 @@
|
|||||||
CloseClipboard
|
// kernel32
|
||||||
CreateProcess
|
|
||||||
CreateRemoteThread
|
|
||||||
EmptyClipboard
|
|
||||||
GetConsoleMode
|
|
||||||
GetDC
|
|
||||||
GetDeviceCaps
|
|
||||||
GetModuleHandle
|
|
||||||
GetProcAddress
|
|
||||||
GetStdHandle
|
|
||||||
GlobalLock
|
GlobalLock
|
||||||
|
OpenProcess
|
||||||
|
GetStdHandle
|
||||||
GlobalUnlock
|
GlobalUnlock
|
||||||
OpenClipboard
|
|
||||||
ResumeThread
|
ResumeThread
|
||||||
SetClipboardData
|
Module32Next
|
||||||
|
Module32First
|
||||||
|
CreateProcess
|
||||||
|
LoadLibraryEx
|
||||||
|
VirtualFreeEx
|
||||||
|
VirtualAllocEx
|
||||||
|
GetProcAddress
|
||||||
|
GetConsoleMode
|
||||||
SetConsoleMode
|
SetConsoleMode
|
||||||
TerminateProcess
|
TerminateProcess
|
||||||
VirtualAllocEx
|
CreateRemoteThread
|
||||||
VirtualFreeEx
|
|
||||||
WaitForSingleObject
|
|
||||||
WriteProcessMemory
|
WriteProcessMemory
|
||||||
|
WaitForSingleObject
|
||||||
GetCurrentConsoleFontEx
|
GetCurrentConsoleFontEx
|
||||||
|
CreateToolhelp32Snapshot
|
||||||
|
|
||||||
OpenProcess
|
// psapi
|
||||||
GetModuleFileNameEx
|
GetModuleFileNameEx
|
||||||
|
|
||||||
LoadLibraryEx
|
// user32
|
||||||
|
OpenClipboard
|
||||||
|
CloseClipboard
|
||||||
|
EmptyClipboard
|
||||||
|
SetClipboardData
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<TargetFramework>net9.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<FileVersion>5.7.0</FileVersion>
|
<FileVersion>5.7.2</FileVersion>
|
||||||
<AssemblyVersion>5.7.0</AssemblyVersion>
|
<AssemblyVersion>5.7.2</AssemblyVersion>
|
||||||
<ApplicationIcon>res\icon.ico</ApplicationIcon>
|
<ApplicationIcon>res\icon.ico</ApplicationIcon>
|
||||||
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<!-- [Update to 3.31.0 breaks AOT build](https://github.com/protocolbuffers/protobuf/issues/21824) -->
|
<!-- [Update to 3.31.0 breaks AOT build](https://github.com/protocolbuffers/protobuf/issues/21824) -->
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.30.2" />
|
<PackageReference Include="Google.Protobuf" Version="3.30.2"/>
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.72.0">
|
<PackageReference Include="Grpc.Tools" Version="2.72.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -19,8 +19,15 @@ message AchievementItem {
|
|||||||
|
|
||||||
message MethodRvaConfig {
|
message MethodRvaConfig {
|
||||||
uint32 do_cmd = 1;
|
uint32 do_cmd = 1;
|
||||||
uint32 to_uint16 = 2;
|
|
||||||
uint32 update_normal_prop = 3;
|
uint32 update_normal_prop = 3;
|
||||||
|
uint32 new_string = 4;
|
||||||
|
uint32 find_game_object = 5;
|
||||||
|
uint32 event_system_update = 6;
|
||||||
|
uint32 simulate_pointer_click = 7;
|
||||||
|
uint32 to_int32 = 8;
|
||||||
|
uint32 tcp_state_ptr = 9;
|
||||||
|
uint32 shared_info_ptr = 10;
|
||||||
|
uint32 decompress = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NativeLibConfig {
|
message NativeLibConfig {
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ syntax = "proto3";
|
|||||||
|
|
||||||
option csharp_namespace = "Proto";
|
option csharp_namespace = "Proto";
|
||||||
|
|
||||||
|
message CdnFileInfo {
|
||||||
|
uint32 size = 1;
|
||||||
|
uint32 hash = 2;
|
||||||
|
repeated string urls = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message UpdateInfo {
|
message UpdateInfo {
|
||||||
uint32 version_code = 1;
|
uint32 version_code = 1;
|
||||||
string version_name = 2;
|
string version_name = 2;
|
||||||
@@ -10,4 +16,5 @@ 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;
|
||||||
|
map<string, CdnFileInfo> cdn_files = 8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
@@ -23,12 +22,7 @@ public static partial class AppConfig {
|
|||||||
} else {
|
} else {
|
||||||
GamePath = ReadGamePathFromProcess();
|
GamePath = ReadGamePathFromProcess();
|
||||||
}
|
}
|
||||||
Span<byte> buffer = stackalloc byte[0x10000];
|
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{Utils.GetGameHash(GamePath)}"));
|
||||||
using var stream = File.OpenRead(GamePath);
|
|
||||||
if (stream.Read(buffer) == buffer.Length) {
|
|
||||||
var hash = Convert.ToHexString(MD5.HashData(buffer));
|
|
||||||
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{hash}"));
|
|
||||||
}
|
|
||||||
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
|
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
|
||||||
return;
|
return;
|
||||||
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
|
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
|
||||||
@@ -38,9 +32,7 @@ public static partial class AppConfig {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
|
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
|
||||||
Span<byte> buffer = stackalloc byte[0x10000];
|
if (Utils.GetGameHash(cacheData[0]) != uint.Parse(cacheData[1])) {
|
||||||
using var stream = File.OpenRead(cacheData[0]);
|
|
||||||
if (stream.Read(buffer) != buffer.Length || Convert.ToHexString(MD5.HashData(buffer)) != cacheData[1]) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
path = cacheData[0];
|
path = cacheData[0];
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public static class GlobalVars {
|
|||||||
public static readonly string CachePath = Path.Combine(DataPath, "cache");
|
public static readonly string CachePath = Path.Combine(DataPath, "cache");
|
||||||
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
|
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
|
||||||
|
|
||||||
public const uint AppVersionCode = 240;
|
public const uint AppVersionCode = 243;
|
||||||
public const string AppVersionName = "5.7";
|
public const string AppVersionName = "5.7.3";
|
||||||
|
|
||||||
public const string PipeName = "YaeAchievementPipe";
|
public const string PipeName = "YaeAchievementPipe";
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ using static YaeAchievement.Utils;
|
|||||||
|
|
||||||
namespace YaeAchievement;
|
namespace YaeAchievement;
|
||||||
|
|
||||||
// TODO: WndHook
|
|
||||||
|
|
||||||
internal static class Program {
|
internal static class Program {
|
||||||
|
|
||||||
public static async Task Main(string[] args) {
|
public static async Task Main(string[] args) {
|
||||||
@@ -47,10 +45,10 @@ internal static class Program {
|
|||||||
Environment.Exit(-1);
|
Environment.Exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
|
await CheckUpdate(ToBooleanOrDefault(args.ElementAtOrDefault(2)));
|
||||||
|
|
||||||
AppConfig.Load(args.GetOrNull(0) ?? "auto");
|
AppConfig.Load(args.ElementAtOrDefault(0) ?? "auto");
|
||||||
Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514);
|
Export.ExportTo = ToIntOrDefault(args.ElementAtOrDefault(1), 114514);
|
||||||
|
|
||||||
AchievementAllDataNotify? data = null;
|
AchievementAllDataNotify? data = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||||
using Windows.Win32.System.LibraryLoader;
|
using Windows.Win32.System.LibraryLoader;
|
||||||
using Windows.Win32.System.Threading;
|
using Windows.Win32.System.Threading;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
@@ -13,7 +14,7 @@ using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
|
|||||||
|
|
||||||
namespace YaeAchievement.Utilities;
|
namespace YaeAchievement.Utilities;
|
||||||
|
|
||||||
public sealed unsafe class GameProcess {
|
internal sealed unsafe class GameProcess {
|
||||||
|
|
||||||
public uint Id { get; }
|
public uint Id { get; }
|
||||||
|
|
||||||
@@ -73,7 +74,29 @@ public sealed unsafe class GameProcess {
|
|||||||
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
|
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
|
||||||
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
|
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
|
||||||
}
|
}
|
||||||
var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
|
// Get lib base address in target process
|
||||||
|
byte* baseAddress = null;
|
||||||
|
using (var hSnap = Native.CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, Id)) {
|
||||||
|
if (hSnap.IsInvalid) {
|
||||||
|
throw new Win32Exception { Data = { { "api", "CreateToolhelp32Snapshot" } } };
|
||||||
|
}
|
||||||
|
var moduleEntry = new MODULEENTRY32 {
|
||||||
|
dwSize = (uint) sizeof(MODULEENTRY32)
|
||||||
|
};
|
||||||
|
if (Native.Module32First(hSnap, ref moduleEntry)) {
|
||||||
|
do {
|
||||||
|
if (new string((sbyte*) &moduleEntry.szExePath._0) == libPath) {
|
||||||
|
baseAddress = moduleEntry.modBaseAddr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (Native.Module32Next(hSnap, ref moduleEntry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baseAddress == null) {
|
||||||
|
throw new InvalidOperationException("No matching module found in target process.");
|
||||||
|
}
|
||||||
|
//
|
||||||
|
using var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
|
||||||
if (libHandle.IsInvalid) {
|
if (libHandle.IsInvalid) {
|
||||||
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
|
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
|
||||||
}
|
}
|
||||||
@@ -81,7 +104,9 @@ public sealed unsafe class GameProcess {
|
|||||||
if (libMainProc.IsNull) {
|
if (libMainProc.IsNull) {
|
||||||
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
|
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
|
||||||
}
|
}
|
||||||
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) libMainProc.Value; // THREAD_START_ROUTINE
|
var libMainProcRVA = libMainProc.Value - libHandle.DangerousGetHandle();
|
||||||
|
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) (baseAddress + libMainProcRVA); // THREAD_START_ROUTINE
|
||||||
|
//
|
||||||
var hThread2 = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress2, null, 0);
|
var hThread2 = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress2, null, 0);
|
||||||
if (hThread2.IsNull) {
|
if (hThread2.IsNull) {
|
||||||
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };
|
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
@@ -28,30 +30,68 @@ public static class Utils {
|
|||||||
|
|
||||||
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
|
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
|
||||||
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
|
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
|
||||||
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
|
SentrySdk.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);
|
||||||
|
var cacheKey = useCache ? path : null;
|
||||||
|
//
|
||||||
|
if (_updateInfo?.CdnFiles.TryGetValue(path, out var cdnFile) == true) {
|
||||||
|
try {
|
||||||
|
var data = await cdnFile.Urls
|
||||||
|
.Select(url => GetFileFromCdn(url, cacheKey, cdnFile.Hash, cdnFile.Size))
|
||||||
|
.WhenFirstSuccessful()
|
||||||
|
.Unwrap();
|
||||||
|
transaction.Finish();
|
||||||
|
return data;
|
||||||
|
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException or InvalidDataException) {}
|
||||||
|
}
|
||||||
|
//
|
||||||
try {
|
try {
|
||||||
var data = await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
|
var data = await WhenFirstSuccessful([
|
||||||
|
GetFileReal($"https://rin.holohat.work/{path}", cacheKey),
|
||||||
|
GetFileReal($"https://ena-rin.holohat.work//{path}", cacheKey),
|
||||||
|
GetFileReal($"https://cn-cd-1259389942.file.myqcloud.com/{path}", cacheKey)
|
||||||
|
]).Unwrap();
|
||||||
transaction.Finish();
|
transaction.Finish();
|
||||||
return data;
|
return data;
|
||||||
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
|
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
|
||||||
|
AnsiConsole.WriteLine(App.NetworkError, e.Message);
|
||||||
}
|
}
|
||||||
try {
|
transaction.Finish();
|
||||||
var data = await Task.WhenAny(
|
Environment.Exit(-1);
|
||||||
GetFile("https://rin.holohat.work/{0}", path, useCache),
|
|
||||||
GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
|
|
||||||
).Unwrap();
|
|
||||||
transaction.Finish();
|
|
||||||
return data;
|
|
||||||
} catch (Exception ex) when (ex is HttpRequestException or SocketException or TaskCanceledException) {
|
|
||||||
transaction.Finish();
|
|
||||||
AnsiConsole.WriteLine(App.NetworkError, ex.Message);
|
|
||||||
Environment.Exit(-1);
|
|
||||||
}
|
|
||||||
throw new UnreachableException();
|
throw new UnreachableException();
|
||||||
static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) {
|
static async Task<byte[]> GetFileFromCdn(string url, string? cacheKey, uint hash, uint size) {
|
||||||
using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey));
|
var data = await GetFileReal(url, cacheKey);
|
||||||
|
if (data.Length != size || Crc32.Compute(data) != hash) {
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
if (data.Length > 44 && Unsafe.As<byte, uint>(ref data[0]) == 0x38464947) { // GIF8
|
||||||
|
var seed = Unsafe.As<byte, uint>(ref data[44]) ^ 0x01919810;
|
||||||
|
var hush = Unsafe.As<byte, uint>(ref data[48]) - 0x32123432; // ?!!
|
||||||
|
var span = data.AsSpan()[52..];
|
||||||
|
Span<byte> xorTable = stackalloc byte[4096];
|
||||||
|
new Random((int) seed).NextBytes(xorTable);
|
||||||
|
for (var i = 0; i < span.Length; i++) {
|
||||||
|
span[i] ^= xorTable[i % 4096];
|
||||||
|
}
|
||||||
|
using var dataStream = new MemoryStream();
|
||||||
|
unsafe {
|
||||||
|
fixed (byte* p = span) {
|
||||||
|
var cmpStream = new UnmanagedMemoryStream(p, span.Length);
|
||||||
|
using var decompressor = new BrotliStream(cmpStream, CompressionMode.Decompress);
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
decompressor.CopyTo(dataStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = dataStream.ToArray();
|
||||||
|
if (Crc32.Compute(data) != hush) {
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
static async Task<byte[]> GetFileReal(string url, string? cacheKey) {
|
||||||
|
using var reqwest = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
CacheItem? cache = null;
|
CacheItem? cache = null;
|
||||||
if (useCache && CacheFile.TryRead(objectKey, out cache)) {
|
if (cacheKey != null && CacheFile.TryRead(cacheKey, out cache)) {
|
||||||
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
|
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
|
||||||
}
|
}
|
||||||
using var response = await CHttpClient.SendAsync(reqwest);
|
using var response = await CHttpClient.SendAsync(reqwest);
|
||||||
@@ -60,18 +100,14 @@ public static class Utils {
|
|||||||
}
|
}
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||||
if (useCache) {
|
if (cacheKey != null) {
|
||||||
var etag = response.Headers.ETag!.Tag;
|
var etag = response.Headers.ETag!.Tag;
|
||||||
CacheFile.Write(objectKey, bytes, etag);
|
CacheFile.Write(cacheKey, bytes, etag);
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T? GetOrNull<T>(this T[] array, uint index) where T : class {
|
|
||||||
return array.Length > index ? array[index] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ToIntOrDefault(string? value, int defaultValue = 0) {
|
public static int ToIntOrDefault(string? value, int defaultValue = 0) {
|
||||||
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
|
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
|
||||||
}
|
}
|
||||||
@@ -96,7 +132,7 @@ public static class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once NotAccessedField.Local
|
// ReSharper disable once NotAccessedField.Local
|
||||||
private static UpdateInfo _updateInfo = null!;
|
private static UpdateInfo? _updateInfo;
|
||||||
|
|
||||||
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
|
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
|
||||||
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
|
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
|
||||||
@@ -109,7 +145,7 @@ public static class Utils {
|
|||||||
public static async Task CheckUpdate(bool useLocalLib) {
|
public static async Task CheckUpdate(bool useLocalLib) {
|
||||||
try {
|
try {
|
||||||
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
|
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
|
||||||
var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
|
var versionInfo = _updateInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
|
||||||
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
|
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
|
||||||
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
|
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
|
||||||
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
|
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
|
||||||
@@ -136,7 +172,6 @@ public static class Utils {
|
|||||||
var data = await GetBucketFile("schicksal/lic.dll");
|
var data = await GetBucketFile("schicksal/lic.dll");
|
||||||
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
|
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
|
||||||
}
|
}
|
||||||
_updateInfo = versionInfo;
|
|
||||||
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
|
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
|
||||||
// IO_SharingViolation_File
|
// IO_SharingViolation_File
|
||||||
// The process cannot access the file '{0}' because it is being used by another process.
|
// The process cannot access the file '{0}' because it is being used by another process.
|
||||||
@@ -146,17 +181,14 @@ public static class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||||
public static bool ShellOpen(string path, string? args = null) {
|
public static bool ShellOpen(string path, string args = "") {
|
||||||
try {
|
try {
|
||||||
var startInfo = new ProcessStartInfo {
|
|
||||||
FileName = path,
|
|
||||||
UseShellExecute = true
|
|
||||||
};
|
|
||||||
if (args != null) {
|
|
||||||
startInfo.Arguments = args;
|
|
||||||
}
|
|
||||||
return new Process {
|
return new Process {
|
||||||
StartInfo = startInfo
|
StartInfo = new ProcessStartInfo {
|
||||||
|
FileName = path,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Arguments = args
|
||||||
|
}
|
||||||
}.Start();
|
}.Start();
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
return false;
|
return false;
|
||||||
@@ -228,13 +260,21 @@ public static class Utils {
|
|||||||
break;
|
break;
|
||||||
case 0xFD:
|
case 0xFD:
|
||||||
writer.Write(methodRva.DoCmd);
|
writer.Write(methodRva.DoCmd);
|
||||||
writer.Write(methodRva.ToUint16);
|
|
||||||
writer.Write(methodRva.UpdateNormalProp);
|
writer.Write(methodRva.UpdateNormalProp);
|
||||||
|
writer.Write(methodRva.NewString);
|
||||||
|
writer.Write(methodRva.FindGameObject);
|
||||||
|
writer.Write(methodRva.EventSystemUpdate);
|
||||||
|
writer.Write(methodRva.SimulatePointerClick);
|
||||||
|
writer.Write(methodRva.ToInt32);
|
||||||
|
writer.Write(methodRva.TcpStatePtr);
|
||||||
|
writer.Write(methodRva.SharedInfoPtr);
|
||||||
|
writer.Write(methodRva.Decompress);
|
||||||
break;
|
break;
|
||||||
case 0xFE:
|
case 0xFE:
|
||||||
_proc!.ResumeMainThread();
|
_proc!.ResumeMainThread();
|
||||||
break;
|
break;
|
||||||
case 0xFF:
|
case 0xFF:
|
||||||
|
writer.Write(true);
|
||||||
_isUnexpectedExit = false;
|
_isUnexpectedExit = false;
|
||||||
onFinish();
|
onFinish();
|
||||||
return;
|
return;
|
||||||
@@ -259,11 +299,15 @@ public static class Utils {
|
|||||||
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
|
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetGameHash(string exePath) {
|
public static uint GetGameHash(string exePath) {
|
||||||
Span<byte> buffer = stackalloc byte[0x10000];
|
try {
|
||||||
using var stream = File.OpenRead(exePath);
|
Span<byte> buffer = stackalloc byte[0x10000];
|
||||||
_ = stream.Read(buffer);
|
using var stream = File.OpenRead(exePath);
|
||||||
return Crc32.Compute(buffer);
|
_ = stream.ReadAtLeast(buffer, 0x10000, false);
|
||||||
|
return Crc32.Compute(buffer);
|
||||||
|
} catch (IOException) {
|
||||||
|
return 0xFFFFFFFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static unsafe void SetQuickEditMode(bool enable) {
|
internal static unsafe void SetQuickEditMode(bool enable) {
|
||||||
@@ -289,4 +333,26 @@ public static class Utils {
|
|||||||
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); // todo: use better way like auto set console font etc.
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); // todo: use better way like auto set console font etc.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/76953892
|
||||||
|
private static async Task<Task<TResult>> WhenFirstSuccessful<TResult>(this IEnumerable<Task<TResult>> tasks) {
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Task<TResult>? selectedTask = null;
|
||||||
|
var continuations = tasks
|
||||||
|
.TakeWhile(_ => !cts.IsCancellationRequested)
|
||||||
|
.Select(task => {
|
||||||
|
return task.ContinueWith(t => {
|
||||||
|
if (t.IsCompletedSuccessfully) {
|
||||||
|
if (Interlocked.CompareExchange(ref selectedTask, t, null) is null) {
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cts.Token, TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||||
|
});
|
||||||
|
var whenAll = Task.WhenAll(continuations);
|
||||||
|
try {
|
||||||
|
await whenAll.ConfigureAwait(false);
|
||||||
|
} catch when (whenAll.IsCanceled) { /* ignore */ }
|
||||||
|
return selectedTask!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,20 +25,47 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DirectPInvoke Include="NTDLL"/>
|
<DirectPInvoke Include="NTDLL" />
|
||||||
<DirectPInvoke Include="USER32"/>
|
<DirectPInvoke Include="USER32" />
|
||||||
<DirectPInvoke Include="KERNEL32"/>
|
<DirectPInvoke Include="KERNEL32" />
|
||||||
<DirectPInvoke Include="libMinHook.x64"/>
|
<DirectPInvoke Include="libMinHook.x64" />
|
||||||
<NativeLibrary Include="lib\libMinHook.x64.lib"/>
|
<NativeLibrary Include="lib\libMinHook.x64.lib" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute"/>
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="System.Diagnostics"/>
|
<Using Include="System.Diagnostics" />
|
||||||
<Using Include="System.Diagnostics.CodeAnalysis"/>
|
<Using Include="System.Diagnostics.CodeAnalysis" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>Yae.Lib</PackageId>
|
||||||
|
<Version>5.4.4</Version>
|
||||||
|
<Authors>HoloHat</Authors>
|
||||||
|
<DevelopmentDependency>true</DevelopmentDependency>
|
||||||
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
|
<RepositoryUrl>https://github.com/HolographicHat/Yae</RepositoryUrl>
|
||||||
|
<Description>Yae Lib</Description>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="$(PublishDir)\$(TargetName)$(NativeBinaryExt)" Pack="true" PackagePath="runtimes\win-x64\native" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference OutputItemType="Analyzer" Include="..\YaeAchievement.SourceGeneration\YaeAchievement.SourceGeneration.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="GenerateNuGetPackage" AfterTargets="CopyNativeBinary">
|
||||||
|
<Exec Command="dotnet pack --no-build --nologo" UseUtf8Encoding="Always" EchoOff="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ using System.Buffers.Binary;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Yae.Utilities;
|
using Yae.Utilities;
|
||||||
|
using static Yae.GameMethod;
|
||||||
|
|
||||||
namespace Yae;
|
namespace Yae;
|
||||||
|
|
||||||
internal static unsafe class Application {
|
internal static unsafe class Application {
|
||||||
|
|
||||||
|
private static bool _initialized;
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
|
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
|
||||||
private static uint Awake(nint hModule) {
|
private static uint Awake(nint hModule) {
|
||||||
|
if (Interlocked.Exchange(ref _initialized, true)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Native.RegisterUnhandledExceptionHandler();
|
Native.RegisterUnhandledExceptionHandler();
|
||||||
Log.UseConsoleOutput();
|
Log.UseConsoleOutput();
|
||||||
Log.Trace("~");
|
Log.Trace("~");
|
||||||
@@ -21,20 +27,27 @@ internal static unsafe class Application {
|
|||||||
Log.ResetConsole();
|
Log.ResetConsole();
|
||||||
//
|
//
|
||||||
RecordChecksum();
|
RecordChecksum();
|
||||||
MinHook.Attach(GameMethod.DoCmd, &OnDoCmd, out _doCmd);
|
MinHook.Attach(DoCmd, &OnDoCmd, out _doCmd);
|
||||||
MinHook.Attach(GameMethod.ToUInt16, &OnToUInt16, out _toUInt16);
|
MinHook.Attach(ToUInt32, &OnToInt32, out _toInt32);
|
||||||
MinHook.Attach(GameMethod.UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
|
MinHook.Attach(UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
|
||||||
|
MinHook.Attach(EventSystemUpdate, &OnEventSystemUpdate, out _eventSystemUpdate);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "YaeWndHook")]
|
||||||
|
private static nint WndHook(int nCode, nint wParam, nint lParam) {
|
||||||
|
((delegate*unmanaged<nint, uint>) &Awake)(0);
|
||||||
|
return User32.CallNextHookEx(0, nCode, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
#region RecvPacket
|
#region RecvPacket
|
||||||
|
|
||||||
private static delegate*unmanaged<byte*, int, ushort> _toUInt16;
|
private static delegate*unmanaged<byte*, int, int> _toInt32;
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
[UnmanagedCallersOnly]
|
||||||
private static ushort OnToUInt16(byte* val, int startIndex) {
|
private static int OnToInt32(byte* val, int startIndex) {
|
||||||
var ret = _toUInt16(val, startIndex);
|
var ret = _toInt32(val, startIndex);
|
||||||
if (ret != 0xAB89 || *(ushort*) (val += 0x20) != 0x6745) {
|
if (startIndex != 6 || *(ushort*) (val += 0x20) != 0x6745) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
var cmdId = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 2));
|
var cmdId = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 2));
|
||||||
@@ -46,11 +59,61 @@ internal static unsafe class Application {
|
|||||||
return ret;
|
return ret;
|
||||||
static Span<byte> GetData(byte* val) {
|
static Span<byte> GetData(byte* val) {
|
||||||
var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4));
|
var headLen = BinaryPrimitives.ReverseEndianness(*(ushort*) (val + 4));
|
||||||
|
var headPtr = val + 10;
|
||||||
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
|
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
|
||||||
return new Span<byte>(val + 10 + headLen, (int) dataLen);
|
var dataPtr = val + 10 + headLen;
|
||||||
|
var unzipLen = GetDecompressedSize(new Span<byte>(headPtr, headLen));
|
||||||
|
if (unzipLen == 0) {
|
||||||
|
return new Span<byte>(dataPtr, (int) dataLen);
|
||||||
|
}
|
||||||
|
var unzipBuf = NativeMemory.Alloc(unzipLen);
|
||||||
|
if (!Decompress(*TcpStatePtr, *SharedInfoPtr, dataPtr, dataLen, unzipBuf, unzipLen)) {
|
||||||
|
throw new InvalidDataException("Decompress failed.");
|
||||||
|
}
|
||||||
|
return new Span<byte>(unzipBuf, (int) unzipLen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static uint GetDecompressedSize(Span<byte> header) {
|
||||||
|
var offset = 0;
|
||||||
|
ulong tag;
|
||||||
|
while (offset != header.Length && (tag = ReadRawVarInt64(header, ref offset)) != 0) {
|
||||||
|
if (tag == 64) {
|
||||||
|
return (uint) ReadRawVarInt64(header, ref offset);
|
||||||
|
}
|
||||||
|
switch (tag & 7) {
|
||||||
|
case 0:
|
||||||
|
ReadRawVarInt64(header, ref offset);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
offset += 8;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
offset += (int) ReadRawVarInt64(header, ref offset);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
throw new NotSupportedException();
|
||||||
|
case 5:
|
||||||
|
offset += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ReadRawVarInt64(this Span<byte> span, ref int offset) {
|
||||||
|
ulong result = 0;
|
||||||
|
for (var i = 0; i < 8; i++) {
|
||||||
|
var b = span[offset++];
|
||||||
|
result |= (ulong) (b & 0x7F) << (i * 7);
|
||||||
|
if (b < 0x80) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidDataException("CodedInputStream encountered a malformed varint.");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Prop
|
#region Prop
|
||||||
@@ -106,7 +169,7 @@ internal static unsafe class Application {
|
|||||||
Buffer = buffer,
|
Buffer = buffer,
|
||||||
Length = 256
|
Length = 256
|
||||||
};
|
};
|
||||||
_ = GameMethod.DoCmd(23, Unsafe.AsPointer(ref data), sizeof(RecordChecksumCmdData));
|
_ = DoCmd(23, Unsafe.AsPointer(ref data), sizeof(RecordChecksumCmdData));
|
||||||
RecordedChecksum[i] = data;
|
RecordedChecksum[i] = data;
|
||||||
//REPL//Log.Trace($"nType={i}, value={new string((sbyte*) buffer, 0, data.Length)}");
|
//REPL//Log.Trace($"nType={i}, value={new string((sbyte*) buffer, 0, data.Length)}");
|
||||||
}
|
}
|
||||||
@@ -131,4 +194,24 @@ internal static unsafe class Application {
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region EnterGate
|
||||||
|
|
||||||
|
private static long _lastTryEnterTime;
|
||||||
|
|
||||||
|
private static delegate*unmanaged<nint, void> _eventSystemUpdate;
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly]
|
||||||
|
public static void OnEventSystemUpdate(nint @this) {
|
||||||
|
_eventSystemUpdate(@this);
|
||||||
|
if (Environment.TickCount64 - _lastTryEnterTime > 200) {
|
||||||
|
var obj = FindGameObject(NewString("BtnStart"u8.AsPointer()));
|
||||||
|
if (obj != 0 && SimulatePointerClick(@this, obj)) {
|
||||||
|
MinHook.Detach((nint) EventSystemUpdate);
|
||||||
|
}
|
||||||
|
_lastTryEnterTime = Environment.TickCount64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,24 @@ internal static unsafe class GameMethod {
|
|||||||
|
|
||||||
public static delegate*unmanaged<int, void*, int, int> DoCmd { get; set; }
|
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; }
|
public static delegate*unmanaged<nint, int, double, double, int, void> UpdateNormalProp { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<nint, nint> NewString { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<nint, nint> FindGameObject { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<nint, void> EventSystemUpdate { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<nint, nint, bool> SimulatePointerClick { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<byte*, int, int> ToUInt32 { get; set; }
|
||||||
|
|
||||||
|
public static void** TcpStatePtr { get; set; }
|
||||||
|
|
||||||
|
public static void** SharedInfoPtr { get; set; }
|
||||||
|
|
||||||
|
public static delegate*unmanaged<void*, void*, void*, uint, void*, uint, bool> Decompress { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class Goshujin {
|
internal static class Goshujin {
|
||||||
@@ -26,8 +40,10 @@ internal static class Goshujin {
|
|||||||
private static NamedPipeClientStream _pipeStream = null!;
|
private static NamedPipeClientStream _pipeStream = null!;
|
||||||
private static BinaryReader _pipeReader = null!;
|
private static BinaryReader _pipeReader = null!;
|
||||||
private static BinaryWriter _pipeWriter = null!;
|
private static BinaryWriter _pipeWriter = null!;
|
||||||
|
private static Lock _lock = null!;
|
||||||
|
|
||||||
public static void Init(string pipeName = "YaeAchievementPipe") {
|
public static void Init(string pipeName = "YaeAchievementPipe") {
|
||||||
|
_lock = new Lock();
|
||||||
_pipeStream = new NamedPipeClientStream(pipeName);
|
_pipeStream = new NamedPipeClientStream(pipeName);
|
||||||
_pipeReader = new BinaryReader(_pipeStream);
|
_pipeReader = new BinaryReader(_pipeStream);
|
||||||
_pipeWriter = new BinaryWriter(_pipeStream);
|
_pipeWriter = new BinaryWriter(_pipeStream);
|
||||||
@@ -36,26 +52,32 @@ internal static class Goshujin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void PushAchievementData(Span<byte> data) {
|
public static void PushAchievementData(Span<byte> data) {
|
||||||
_pipeWriter.Write((byte) 1);
|
using (_lock.EnterScope()) {
|
||||||
_pipeWriter.Write(data.Length);
|
_pipeWriter.Write((byte) 1);
|
||||||
_pipeWriter.Write(data);
|
_pipeWriter.Write(data.Length);
|
||||||
_achievementDataPushed = true;
|
_pipeWriter.Write(data);
|
||||||
ExitIfFinished();
|
_achievementDataPushed = true;
|
||||||
|
ExitIfFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PushStoreData(Span<byte> data) {
|
public static void PushStoreData(Span<byte> data) {
|
||||||
_pipeWriter.Write((byte) 2);
|
using (_lock.EnterScope()) {
|
||||||
_pipeWriter.Write(data.Length);
|
_pipeWriter.Write((byte) 2);
|
||||||
_pipeWriter.Write(data);
|
_pipeWriter.Write(data.Length);
|
||||||
_storeDataPushed = true;
|
_pipeWriter.Write(data);
|
||||||
ExitIfFinished();
|
_storeDataPushed = true;
|
||||||
|
ExitIfFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PushPlayerProp(int type, double value) {
|
public static void PushPlayerProp(int type, double value) {
|
||||||
_pipeWriter.Write((byte) 3);
|
using (_lock.EnterScope()) {
|
||||||
_pipeWriter.Write(type);
|
_pipeWriter.Write((byte) 3);
|
||||||
_pipeWriter.Write(value);
|
_pipeWriter.Write(type);
|
||||||
ExitIfFinished();
|
_pipeWriter.Write(value);
|
||||||
|
ExitIfFinished();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LoadCmdTable() {
|
public static void LoadCmdTable() {
|
||||||
@@ -67,8 +89,15 @@ internal static class Goshujin {
|
|||||||
public static unsafe void LoadMethodTable() {
|
public static unsafe void LoadMethodTable() {
|
||||||
_pipeWriter.Write((byte) 0xFD);
|
_pipeWriter.Write((byte) 0xFD);
|
||||||
GameMethod.DoCmd = (delegate*unmanaged<int, void*, int, int>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
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());
|
GameMethod.UpdateNormalProp = (delegate*unmanaged<nint, int, double, double, int, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.NewString = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.FindGameObject = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.EventSystemUpdate = (delegate*unmanaged<nint, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.SimulatePointerClick = (delegate*unmanaged<nint, nint, bool>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.ToUInt32 = (delegate*unmanaged<byte*, int, int>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.TcpStatePtr = (void**) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.SharedInfoPtr = (void**) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
|
GameMethod.Decompress = (delegate*unmanaged<void*, void*, void*, uint, void*, uint, bool>) Native.RVAToVA(_pipeReader.ReadUInt32());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ResumeMainThread() {
|
public static void ResumeMainThread() {
|
||||||
@@ -82,6 +111,7 @@ internal static class Goshujin {
|
|||||||
private static void ExitIfFinished() {
|
private static void ExitIfFinished() {
|
||||||
if (_storeDataPushed && _achievementDataPushed && Application.RequiredPlayerProperties.Count == 0) {
|
if (_storeDataPushed && _achievementDataPushed && Application.RequiredPlayerProperties.Count == 0) {
|
||||||
_pipeWriter.Write((byte) 0xFF);
|
_pipeWriter.Write((byte) 0xFF);
|
||||||
|
_pipeReader.ReadBoolean();
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ internal static unsafe class Native {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static nint AsPointer(this ReadOnlySpan<byte> span) => *(nint*) Unsafe.AsPointer(ref span);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static partial class MinHook {
|
internal static partial class MinHook {
|
||||||
@@ -146,6 +149,7 @@ internal static partial class MinHook {
|
|||||||
/// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program.
|
/// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Uninitialize")]
|
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Uninitialize")]
|
||||||
|
// ReSharper disable once UnusedMember.Local
|
||||||
private static partial uint MinHookUninitialize();
|
private static partial uint MinHookUninitialize();
|
||||||
|
|
||||||
static MinHook() {
|
static MinHook() {
|
||||||
@@ -155,30 +159,6 @@ internal static partial class MinHook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
public static void Attach(nint origin, nint handler, out nint trampoline) {
|
||||||
uint result;
|
uint result;
|
||||||
if ((result = MinHookCreate(origin, handler, out trampoline)) != 0) {
|
if ((result = MinHookCreate(origin, handler, out trampoline)) != 0) {
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ internal static unsafe partial class Kernel32 {
|
|||||||
[return:MarshalAs(UnmanagedType.I4)]
|
[return:MarshalAs(UnmanagedType.I4)]
|
||||||
[LibraryImport("KERNEL32.dll", SetLastError = true)]
|
[LibraryImport("KERNEL32.dll", SetLastError = true)]
|
||||||
internal static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
|
internal static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
|
||||||
|
|
||||||
|
[LibraryImport("KERNEL32.dll", SetLastError = true)]
|
||||||
|
internal static partial nint CreateThread(nint lpThreadAttributes, nint dwStackSize, delegate*unmanaged<nint, uint> lpStartAddress, nint lpParameter, uint dwCreationFlags, uint* lpThreadId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,4 +68,7 @@ internal static unsafe partial class User32 {
|
|||||||
[LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
|
[LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
|
||||||
internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType);
|
internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType);
|
||||||
|
|
||||||
|
[LibraryImport("USER32.dll")]
|
||||||
|
internal static partial nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user