mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-10 00:18:12 +08:00
Compare commits
62 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 | ||
|
|
48043a9deb | ||
|
|
c09e008d38 | ||
|
|
ac818735c7 | ||
|
|
c4fb5a0366 | ||
|
|
0522078582 | ||
|
|
97d063ec76 | ||
|
|
a6caa07599 | ||
|
|
7fc296f6e9 | ||
|
|
1216ffb872 | ||
|
|
f8341c3044 | ||
|
|
40269410c5 | ||
|
|
a32e27323c | ||
|
|
4a1da61904 | ||
|
|
9eb8955fda | ||
|
|
62c08f54ab | ||
|
|
645fe38c65 | ||
|
|
8f9a26a237 | ||
|
|
8648b3a308 | ||
|
|
829553b3a6 | ||
|
|
87898eedfa | ||
|
|
a10b491886 | ||
|
|
e3e7107b14 | ||
|
|
3231746aa5 | ||
|
|
5c9cdd46d2 | ||
|
|
881a4bc725 | ||
|
|
d08ac17d10 | ||
|
|
980a47bf43 | ||
|
|
0e7be25b23 | ||
|
|
4b052cf6c7 | ||
|
|
4ff2b454f3 | ||
|
|
f718687b3f | ||
|
|
9915b9246e | ||
|
|
e25bc9aeba | ||
|
|
08dd6eca76 | ||
|
|
acd2ccd803 | ||
|
|
13fda3ba12 | ||
|
|
1c821620bf | ||
|
|
8a39ad0a77 | ||
|
|
9d42141258 | ||
|
|
2a91656b2e |
24
.github/workflows/dotnet.yml
vendored
24
.github/workflows/dotnet.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: .NET Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
@@ -8,11 +9,13 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./YaeAchievement
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
@@ -21,10 +24,17 @@ jobs:
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build -c Release --no-restore
|
||||
- name: Publish
|
||||
- name: Publish-AOT
|
||||
run: dotnet publish --property:OutputPath=.\publish\
|
||||
- name: Upload artifact
|
||||
- name: Upload-AOT
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Artifacts
|
||||
path: publish
|
||||
name: aot
|
||||
path: YaeAchievement\publish\publish
|
||||
- name: Publish-NoAOT
|
||||
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
|
||||
- name: Upload-NoAOT
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: normal
|
||||
path: YaeAchievement\naot-publish\publish
|
||||
|
||||
44
.github/workflows/lib-nuget.yml
vendored
44
.github/workflows/lib-nuget.yml
vendored
@@ -2,27 +2,35 @@ name: YaeLib NuGet Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_version:
|
||||
description: 'Version already increased?'
|
||||
required: true
|
||||
type: boolean
|
||||
perform_publish:
|
||||
description: 'Publish to nuget?'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: YaeAchievementLib
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- 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
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Build native library
|
||||
run: dotnet publish
|
||||
- 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
|
||||
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,15 +1,6 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
bin
|
||||
obj
|
||||
.idea
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.vs/
|
||||
|
||||
src/Proto/*
|
||||
.vs
|
||||
publish
|
||||
sync
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
## 导出支持
|
||||
|
||||
> 按照数字键选择导出方式,<kbd>0</kbd> 为默认导出方式
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
@@ -32,11 +30,8 @@
|
||||
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## 问题反馈
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## 常见问题
|
||||
0. Q: 打不开
|
||||
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer)
|
||||
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
A: 不要把软件和原神主程序放一起
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
## Export support
|
||||
|
||||
> Select the export method according to the number keys, <kbd>0</kbd> is the default export method
|
||||
|
||||
0. [Cocogoat](https://cocogoat.work/achievement)
|
||||
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
@@ -32,13 +30,9 @@
|
||||
## Download: [Here](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## Feedback or Problem?
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## Frequently asked questions
|
||||
0. Q: Unable to start
|
||||
A: Download and install [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.8`
|
||||
|
||||
|
||||
1. Q: Error while Genshin started: Data Exception (31-4302)
|
||||
A: Don't place software in the directory containing Genshin Impact.
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
## エクスポートサポート
|
||||
|
||||
> 数字キーに従ってエクスポート方法を選択します。<kbd>0</kbd> はデフォルトのエクスポート方法です
|
||||
|
||||
0. [椰羊](https://cocogoat.work/achievement)
|
||||
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
|
||||
2. [Paimon.moe](https://paimon.moe/achievement/)
|
||||
@@ -32,13 +30,9 @@
|
||||
## ダウンロード: [こちら](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## フィードバックや問題?
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 598720036](https://qm.qq.com/q/mCQ8PwpxJY)
|
||||
|
||||
## よくある質問
|
||||
0. Q: 起動できない
|
||||
A: [.NET Runtime 7](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer) をダウンロードしてインストールするか、`winget install Microsoft.DotNet.Runtime.8` を実行してください。
|
||||
|
||||
|
||||
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
|
||||
A: ソフトウェアを原神のディレクトリに配置しないでください。
|
||||
|
||||
|
||||
14
Tutorial.md
14
Tutorial.md
@@ -8,19 +8,7 @@
|
||||
|
||||

|
||||
|
||||
2.安装启动软件所需文件(若已安装该运行时可忽略此步骤)
|
||||
|
||||
点击该网址:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer 。
|
||||
|
||||
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
|
||||
|
||||
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件,会弹出安装窗口,如下图所示。
|
||||
|
||||

|
||||
|
||||
直接点击安装即可。
|
||||
|
||||
3.打开主程序所需的操作以及成就导出的选择
|
||||
2.打开主程序所需的操作以及成就导出的选择
|
||||
|
||||
双击在第一步下载的名称为“YaeAchievement.exe”的文件,成功打开后会提示原神正在启动,如下图所示。
|
||||
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
## Instructions for Use
|
||||
|
||||
# Instructions for Use
|
||||
|
||||
1.Download YaeAchievement(Latest Version):
|
||||
|
||||
Click Here:https://github.com/HolographicHat/YaeAchievement/releases
|
||||
Click Here:<https://github.com/HolographicHat/YaeAchievement/releases>
|
||||
|
||||
Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that you save this file in a desktop or other easy-to-see folder.
|
||||
Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that
|
||||
you save this file in a desktop or other easy-to-see folder.
|
||||
|
||||

|
||||
|
||||
2.Install .NET Runtime 7 (this step can be ignored if the runtime is already installed)
|
||||
|
||||
Click Here:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
|
||||
|
||||
Or `winget install Microsoft.DotNet.Runtime.7` if you use Windows 11 or have Winget installed.
|
||||
|
||||
The browser automatically pops up and downloads when you enter the web page, as well as saving files in a desktop or other easy-to-see folder.
|
||||
|
||||
When you open a file with the name dotnet-runtime-x.x.x-win-x64.exe after the download is complete, an installation window pops up, as shown below.
|
||||
|
||||

|
||||
|
||||
Just click Install.
|
||||
|
||||
3.The actions required to open the main program and the options for the achievement export
|
||||
|
||||
Double-click the file named "YaeAchievement.exe" downloaded in the first step to open it successfully, indicating that the original god is starting, as shown below.
|
||||
|
||||
@@ -8,20 +8,6 @@
|
||||
|
||||

|
||||
|
||||
2. .NET Runtime 7をインストール(ランタイムが既にインストールされている場合はこのステップを無視できます)
|
||||
|
||||
こちらをクリック:https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.4-windows-x64-installer .
|
||||
|
||||
または、Windows 11を使用しているか、Wingetがインストールされている場合は、`winget install Microsoft.DotNet.Runtime.7`を実行します。
|
||||
|
||||
ウェブページにアクセスすると、ブラウザが自動的にポップアップしてダウンロードされます。ファイルをデスクトップや他の見やすいフォルダに保存します。
|
||||
|
||||
ダウンロードが完了したら、dotnet-runtime-x.x.x-win-x64.exeという名前のファイルを開くと、インストールウィンドウがポップアップします。以下の図のように表示されます。
|
||||
|
||||

|
||||
|
||||
インストールをクリックするだけです。
|
||||
|
||||
3. メインプログラムを開くための操作と実績エクスポートのオプション
|
||||
|
||||
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。
|
||||
|
||||
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,3 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#"/>
|
||||
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
|
||||
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
|
||||
<Project Path="YaeAchievement\YaeAchievement.csproj" />
|
||||
</Solution>
|
||||
@@ -2,5 +2,5 @@
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"className": "Native",
|
||||
"allowMarshaling": false,
|
||||
"public": true
|
||||
"public": false
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
CloseClipboard
|
||||
CreateProcess
|
||||
CreateRemoteThread
|
||||
EmptyClipboard
|
||||
GetConsoleMode
|
||||
GetDC
|
||||
GetDeviceCaps
|
||||
GetModuleHandle
|
||||
GetProcAddress
|
||||
GetStdHandle
|
||||
// kernel32
|
||||
GlobalLock
|
||||
OpenProcess
|
||||
GetStdHandle
|
||||
GlobalUnlock
|
||||
OpenClipboard
|
||||
ResumeThread
|
||||
SetClipboardData
|
||||
Module32Next
|
||||
Module32First
|
||||
CreateProcess
|
||||
LoadLibraryEx
|
||||
VirtualFreeEx
|
||||
VirtualAllocEx
|
||||
GetProcAddress
|
||||
GetConsoleMode
|
||||
SetConsoleMode
|
||||
TerminateProcess
|
||||
VirtualAllocEx
|
||||
VirtualFreeEx
|
||||
CreateRemoteThread
|
||||
WriteProcessMemory
|
||||
WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
GetCurrentConsoleFontEx
|
||||
CreateToolhelp32Snapshot
|
||||
|
||||
// psapi
|
||||
GetModuleFileNameEx
|
||||
|
||||
// user32
|
||||
OpenClipboard
|
||||
CloseClipboard
|
||||
EmptyClipboard
|
||||
SetClipboardData
|
||||
@@ -1,62 +1,66 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
||||
<AssemblyVersion>5.3.0</AssemblyVersion>
|
||||
<FileVersion>5.3.0</FileVersion>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<FileVersion>5.7.2</FileVersion>
|
||||
<AssemblyVersion>5.7.2</AssemblyVersion>
|
||||
<ApplicationIcon>res\icon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<DebugType>embedded</DebugType>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PropertyGroup>
|
||||
<PublishAot>true</PublishAot>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<OptimizationPreference>Size</OptimizationPreference>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.0" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Grpc.Tools" Version="2.67.0">
|
||||
<!-- [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="Grpc.Tools" Version="2.72.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Sentry" Version="5.13.0"/>
|
||||
<PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.22"/>
|
||||
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="res\App.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>App.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Remove="res\Updater.exe" />
|
||||
<None Remove="src\NativeMethods.json" />
|
||||
<None Remove="src\NativeMethods.txt" />
|
||||
<AdditionalFiles Include="src\NativeMethods.json" />
|
||||
<AdditionalFiles Include="src\NativeMethods.txt" />
|
||||
<EmbeddedResource Include="res\Updater.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="res\App.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>App.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Remove="res\updater.exe"/>
|
||||
<EmbeddedResource Include="res\updater.exe" LogicalName="updater"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="res\App.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>App.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="res\App.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>App.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None" />
|
||||
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CETCompat>false</CETCompat>
|
||||
<!-- <TrimmerSingleWarn>false</TrimmerSingleWarn>-->
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
198
YaeAchievement/res/App.Designer.cs
generated
198
YaeAchievement/res/App.Designer.cs
generated
@@ -87,7 +87,25 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You need to login genshin impact before exporting..
|
||||
/// Looks up a localized string similar to No.
|
||||
/// </summary>
|
||||
internal static string CommonNo {
|
||||
get {
|
||||
return ResourceManager.GetString("CommonNo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Yes.
|
||||
/// </summary>
|
||||
internal static string CommonYes {
|
||||
get {
|
||||
return ResourceManager.GetString("CommonYes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please launch GenshinImpact to continue..
|
||||
/// </summary>
|
||||
internal static string ConfigNeedStartGenshin {
|
||||
get {
|
||||
@@ -114,16 +132,7 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export to:
|
||||
///[0] Cocogoat (https://cocogoat.work/achievement, Default)
|
||||
///[1] Snap.HuTao
|
||||
///[2] Paimon.moe
|
||||
///[3] Seelie.me
|
||||
///[4] Csv file
|
||||
///[5] Xunkong
|
||||
///[7] Teyvat Guide
|
||||
///[8] UIAF JSON File
|
||||
///Input a number (0-8): .
|
||||
/// Looks up a localized string similar to Export to:.
|
||||
/// </summary>
|
||||
internal static string ExportChoose {
|
||||
get {
|
||||
@@ -132,7 +141,88 @@ 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 Cocogoat (https://cocogoat.work/achievement).
|
||||
/// </summary>
|
||||
internal static string ExportTargetCocogoat {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetCocogoat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Csv file.
|
||||
/// </summary>
|
||||
internal static string ExportTargetCsv {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetCsv", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Snap.HuTao.
|
||||
/// </summary>
|
||||
internal static string ExportTargetHuTao {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetHuTao", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Paimon.moe.
|
||||
/// </summary>
|
||||
internal static string ExportTargetPaimon {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetPaimon", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Seelie.me.
|
||||
/// </summary>
|
||||
internal static string ExportTargetSeelie {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetSeelie", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Teyvat Guide.
|
||||
/// </summary>
|
||||
internal static string ExportTargetTeyvatGuide {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetTeyvatGuide", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to UIAF JSON File.
|
||||
/// </summary>
|
||||
internal static string ExportTargetUIAFJson {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetUIAFJson", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// </summary>
|
||||
internal static string ExportTargetWxApp1 {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetWxApp1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Xunkong.
|
||||
/// </summary>
|
||||
internal static string ExportTargetXunkong {
|
||||
get {
|
||||
return ResourceManager.GetString("ExportTargetXunkong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fail, please contact developer to get help information (CG_{0}).
|
||||
/// </summary>
|
||||
internal static string ExportToCocogoatFail {
|
||||
get {
|
||||
@@ -257,6 +347,15 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Load failed. Please retry. ({0} {1} {2}).
|
||||
/// </summary>
|
||||
internal static string LoadLibraryFail {
|
||||
get {
|
||||
return ResourceManager.GetString("LoadLibraryFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Network error:.
|
||||
/// </summary>
|
||||
@@ -284,6 +383,42 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use the keyboard arrow keys to move the cursor and the Enter key to select.
|
||||
/// </summary>
|
||||
internal static string SelectionPromptCompatAnsiTip {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectionPromptCompatAnsiTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose an option:.
|
||||
/// </summary>
|
||||
internal static string SelectionPromptCompatChooseOne {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectionPromptCompatChooseOne", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please enter a number between 0 and {0}.
|
||||
/// </summary>
|
||||
internal static string SelectionPromptCompatInvalidChoice {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectionPromptCompatInvalidChoice", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Type a number and press Enter to select.
|
||||
/// </summary>
|
||||
internal static string SelectionPromptCompatNonAnsiTip {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectionPromptCompatNonAnsiTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reward not taken.
|
||||
/// </summary>
|
||||
@@ -320,6 +455,24 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to An error occurred while reading the data. Please try again..
|
||||
/// </summary>
|
||||
internal static string StreamReadDataFail {
|
||||
get {
|
||||
return ResourceManager.GetString("StreamReadDataFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Checking update....
|
||||
/// </summary>
|
||||
internal static string UpdateChecking {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateChecking", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Description:
|
||||
///{0}.
|
||||
@@ -339,6 +492,15 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The process cannot access the file '{0}' because it is being used by another process. Please restart your computer and try again..
|
||||
/// </summary>
|
||||
internal static string UpdateFileShareViolation {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateFileShareViolation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Has update: {0} => {1}.
|
||||
/// </summary>
|
||||
@@ -348,16 +510,6 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
internal static byte[] Updater {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("Updater", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upload error to appcenter....
|
||||
/// </summary>
|
||||
@@ -368,7 +520,7 @@ namespace YaeAchievement.res {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use previous fetched data? (yes|no).
|
||||
/// Looks up a localized string similar to Use previous fetched data?.
|
||||
/// </summary>
|
||||
internal static string UsePreviousData {
|
||||
get {
|
||||
|
||||
@@ -19,22 +19,13 @@
|
||||
<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>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>Export to:
|
||||
[0] Cocogoat (https://cocogoat.work/achievement, Default)
|
||||
[1] Snap.HuTao
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] Csv file
|
||||
[5] Xunkong
|
||||
[7] Teyvat Guide
|
||||
[8] UIAF JSON File
|
||||
Input a number (0-8): </value>
|
||||
<value>Export to:</value>
|
||||
</data>
|
||||
<data name="ExportToCocogoatSuccess" xml:space="preserve">
|
||||
<value>Successfully exported to cocogoat.</value>
|
||||
@@ -70,7 +61,7 @@ Input a number (0-8): </value>
|
||||
<value>Reward not taken</value>
|
||||
</data>
|
||||
<data name="ConfigNeedStartGenshin" xml:space="preserve">
|
||||
<value>You need to login genshin impact before exporting.</value>
|
||||
<value>Please launch GenshinImpact to continue.</value>
|
||||
</data>
|
||||
<data name="DownloadLink" xml:space="preserve">
|
||||
<value>Download: {0}</value>
|
||||
@@ -107,7 +98,7 @@ Input a number (0-8): </value>
|
||||
<value>YaeAchievement ({0})</value>
|
||||
</data>
|
||||
<data name="UsePreviousData" xml:space="preserve">
|
||||
<value>Use previous fetched data? (yes|no)</value>
|
||||
<value>Use previous fetched data?</value>
|
||||
</data>
|
||||
<data name="NetworkError" xml:space="preserve">
|
||||
<value>Network error:</value>
|
||||
@@ -121,10 +112,6 @@ Input a number (0-8): </value>
|
||||
<data name="ExceptionNetwork" xml:space="preserve">
|
||||
<value>Network error ({0}: {1})</value>
|
||||
</data>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="Updater" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>Updater.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="GenshinHashError" xml:space="preserve">
|
||||
<value>Please update genshin and retry.</value>
|
||||
</data>
|
||||
@@ -140,4 +127,61 @@ Input a number (0-8): </value>
|
||||
<data name="WaitMetadataUpdate" xml:space="preserve">
|
||||
<value>Please update game and retry.</value>
|
||||
</data>
|
||||
<data name="UpdateChecking" xml:space="preserve">
|
||||
<value>Checking update...</value>
|
||||
</data>
|
||||
<data name="CommonYes" xml:space="preserve">
|
||||
<value>Yes</value>
|
||||
</data>
|
||||
<data name="CommonNo" xml:space="preserve">
|
||||
<value>No</value>
|
||||
</data>
|
||||
<data name="ExportTargetCocogoat" xml:space="preserve">
|
||||
<value>Cocogoat (https://cocogoat.work/achievement)</value>
|
||||
</data>
|
||||
<data name="ExportTargetHuTao" xml:space="preserve">
|
||||
<value>Snap.HuTao</value>
|
||||
</data>
|
||||
<data name="ExportTargetPaimon" xml:space="preserve">
|
||||
<value>Paimon.moe</value>
|
||||
</data>
|
||||
<data name="ExportTargetSeelie" xml:space="preserve">
|
||||
<value>Seelie.me</value>
|
||||
</data>
|
||||
<data name="ExportTargetCsv" xml:space="preserve">
|
||||
<value>Csv file</value>
|
||||
</data>
|
||||
<data name="ExportTargetXunkong" xml:space="preserve">
|
||||
<value>Xunkong</value>
|
||||
</data>
|
||||
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
|
||||
<value>Teyvat Guide</value>
|
||||
</data>
|
||||
<data name="ExportTargetUIAFJson" xml:space="preserve">
|
||||
<value>UIAF JSON File</value>
|
||||
</data>
|
||||
<data name="ExportTargetWxApp1" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
|
||||
<value>Choose an option:</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
|
||||
<value>Use the keyboard arrow keys to move the cursor and the Enter key to select</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
|
||||
<value>Type a number and press Enter to select</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
|
||||
<value>Please enter a number between 0 and {0}</value>
|
||||
</data>
|
||||
<data name="StreamReadDataFail" xml:space="preserve">
|
||||
<value>An error occurred while reading the data. Please try again.</value>
|
||||
</data>
|
||||
<data name="UpdateFileShareViolation" xml:space="preserve">
|
||||
<value>The process cannot access the file '{0}' because it is being used by another process. Please restart your computer and try again.</value>
|
||||
</data>
|
||||
<data name="LoadLibraryFail" xml:space="preserve">
|
||||
<value>Load failed. Please retry. ({0} {1} {2})</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -12,23 +12,13 @@
|
||||
<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>
|
||||
</data>
|
||||
<data name="ExportChoose" xml:space="preserve">
|
||||
<value>导出至:
|
||||
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
|
||||
[1] Snap Hutao
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] 表格文件
|
||||
[5] 寻空
|
||||
[6] 原魔工具箱
|
||||
[7] Teyvat Guide
|
||||
[8] UIAF JSON 文件
|
||||
输入一个数字 (0-8): </value>
|
||||
<value>要导出到哪里?</value>
|
||||
</data>
|
||||
<data name="ExportToCocogoatSuccess" xml:space="preserve">
|
||||
<value>在浏览器内进行下一步操作</value>
|
||||
@@ -64,7 +54,7 @@
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
<data name="ConfigNeedStartGenshin" xml:space="preserve">
|
||||
<value>在导出前你需要先完成一次登入流程.</value>
|
||||
<value>请打开 原神 后继续操作</value>
|
||||
</data>
|
||||
<data name="DownloadLink" xml:space="preserve">
|
||||
<value>下载地址: {0}</value>
|
||||
@@ -101,7 +91,7 @@
|
||||
<value>YaeAchievement - 原神成就导出工具 ({0})</value>
|
||||
</data>
|
||||
<data name="UsePreviousData" xml:space="preserve">
|
||||
<value>要使用上一次获取到的成就数据吗? (yes|no)</value>
|
||||
<value>要使用上一次获取到的成就数据吗?</value>
|
||||
</data>
|
||||
<data name="NetworkError" xml:space="preserve">
|
||||
<value>网络错误: {0}</value>
|
||||
@@ -130,4 +120,61 @@
|
||||
<data name="WaitMetadataUpdate" xml:space="preserve">
|
||||
<value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value>
|
||||
</data>
|
||||
<data name="UpdateChecking" xml:space="preserve">
|
||||
<value>正在检查更新...</value>
|
||||
</data>
|
||||
<data name="CommonYes" xml:space="preserve">
|
||||
<value>是</value>
|
||||
</data>
|
||||
<data name="CommonNo" xml:space="preserve">
|
||||
<value>否</value>
|
||||
</data>
|
||||
<data name="ExportTargetCocogoat" xml:space="preserve">
|
||||
<value>椰羊 (https://cocogoat.work/achievement)</value>
|
||||
</data>
|
||||
<data name="ExportTargetHuTao" xml:space="preserve">
|
||||
<value>Snap Hutao</value>
|
||||
</data>
|
||||
<data name="ExportTargetPaimon" xml:space="preserve">
|
||||
<value>Paimon.moe</value>
|
||||
</data>
|
||||
<data name="ExportTargetSeelie" xml:space="preserve">
|
||||
<value>Seelie.me</value>
|
||||
</data>
|
||||
<data name="ExportTargetCsv" xml:space="preserve">
|
||||
<value>表格文件</value>
|
||||
</data>
|
||||
<data name="ExportTargetXunkong" xml:space="preserve">
|
||||
<value>寻空</value>
|
||||
</data>
|
||||
<data name="ExportTargetUIAFJson" xml:space="preserve">
|
||||
<value>UIAF JSON 文件</value>
|
||||
</data>
|
||||
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
|
||||
<value>Teyvat Guide</value>
|
||||
</data>
|
||||
<data name="ExportTargetWxApp1" xml:space="preserve">
|
||||
<value>原魔工具箱</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatChooseOne" xml:space="preserve">
|
||||
<value>选择一个选项:</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatAnsiTip" xml:space="preserve">
|
||||
<value>键盘上下键移动光标,键盘回车键选择</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatNonAnsiTip" xml:space="preserve">
|
||||
<value>输入数字并回车以选择选项</value>
|
||||
</data>
|
||||
<data name="SelectionPromptCompatInvalidChoice" xml:space="preserve">
|
||||
<value>请输入 0 到 {0} 之间的数字</value>
|
||||
</data>
|
||||
<data name="StreamReadDataFail" xml:space="preserve">
|
||||
<value>读取数据时发生错误,请重试</value>
|
||||
</data>
|
||||
<data name="UpdateFileShareViolation" xml:space="preserve">
|
||||
<value>文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。</value>
|
||||
</data>
|
||||
<data name="LoadLibraryFail" xml:space="preserve">
|
||||
<value>加载失败,请重试({0} {1} {2})</value>
|
||||
</data>
|
||||
</root>
|
||||
Binary file not shown.
@@ -17,9 +17,29 @@ message AchievementItem {
|
||||
string description = 4;
|
||||
}
|
||||
|
||||
message MethodRvaConfig {
|
||||
uint32 do_cmd = 1;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Proto";
|
||||
|
||||
message CdnFileInfo {
|
||||
uint32 size = 1;
|
||||
uint32 hash = 2;
|
||||
repeated string urls = 4;
|
||||
}
|
||||
|
||||
message UpdateInfo {
|
||||
uint32 version_code = 1;
|
||||
string version_name = 2;
|
||||
@@ -10,6 +16,5 @@ 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;
|
||||
map<string, CdnFileInfo> cdn_files = 8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using YaeAchievement.res;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Spectre.Console;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
@@ -13,48 +15,76 @@ public static partial class AppConfig {
|
||||
internal static void Load(string argumentPath) {
|
||||
if (argumentPath != "auto" && File.Exists(argumentPath)) {
|
||||
GamePath = argumentPath;
|
||||
return;
|
||||
} else if (TryReadGamePathFromCache(out var cachedPath)) {
|
||||
GamePath = cachedPath;
|
||||
} else if (TryReadGamePathFromUnityLog(out var loggedPath)) {
|
||||
GamePath = loggedPath;
|
||||
} else {
|
||||
GamePath = ReadGamePathFromProcess();
|
||||
}
|
||||
var pathCacheFile = new CacheFile("genshin_impact_game_path");
|
||||
if (pathCacheFile.Exists()) {
|
||||
var path = pathCacheFile.Read().Content.ToStringUtf8();
|
||||
if (path != null && File.Exists(path)) {
|
||||
GamePath = path;
|
||||
return;
|
||||
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{Utils.GetGameHash(GamePath)}"));
|
||||
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
|
||||
return;
|
||||
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
|
||||
path = null;
|
||||
try {
|
||||
if (!CacheFile.TryRead("genshin_impact_game_path_v2", out var cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
|
||||
if (Utils.GetGameHash(cacheData[0]) != uint.Parse(cacheData[1])) {
|
||||
return false;
|
||||
}
|
||||
path = cacheData[0];
|
||||
return true;
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var logPath = ProductNames
|
||||
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
|
||||
.Where(File.Exists)
|
||||
.MaxBy(File.GetLastWriteTime);
|
||||
if (logPath == null) {
|
||||
throw new ApplicationException(App.ConfigNeedStartGenshin);
|
||||
static bool TryReadGamePathFromUnityLog([NotNullWhen(true)] out string? path) {
|
||||
path = null;
|
||||
try {
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var logPath = ProductNames
|
||||
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
|
||||
.Where(File.Exists)
|
||||
.MaxBy(File.GetLastWriteTime);
|
||||
if (logPath == null) {
|
||||
return false;
|
||||
}
|
||||
return (path = GetGamePathFromLogFile(logPath) ?? GetGamePathFromLogFile($"{logPath}.last")) != null;
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static string ReadGamePathFromProcess() {
|
||||
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).Start(App.ConfigNeedStartGenshin, _ => {
|
||||
Process? proc;
|
||||
while ((proc = Utils.GetGameProcess()) == null) {
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
var fileName = proc.GetFileName()!;
|
||||
proc.Kill();
|
||||
return fileName;
|
||||
});
|
||||
}
|
||||
GamePath = GetGamePathFromLogFile(logPath)
|
||||
?? GetGamePathFromLogFile($"{logPath}.last")
|
||||
?? throw new ApplicationException(App.ConfigNeedStartGenshin);
|
||||
}
|
||||
|
||||
private static string? GetGamePathFromLogFile(string path) {
|
||||
if (!File.Exists(path)) {
|
||||
return null;
|
||||
}
|
||||
var copiedLogFilePath = Path.GetTempFileName();
|
||||
File.Copy(path, copiedLogFilePath, true);
|
||||
var content = File.ReadAllText(copiedLogFilePath);
|
||||
try {
|
||||
File.Delete(copiedLogFilePath);
|
||||
} catch (Exception) { /* ignore */ }
|
||||
var content = File.ReadAllText(path);
|
||||
var matchResult = GamePathRegex().Match(content);
|
||||
if (!matchResult.Success) {
|
||||
return null;
|
||||
}
|
||||
var entryName = matchResult.Groups["1"].Value.Replace("_Data", ".exe");
|
||||
return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
|
||||
var fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
|
||||
return File.Exists(fullPath) ? fullPath : null;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex(@"(?m).:(?:\\|/).+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex GamePathRegex();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Win32;
|
||||
using Spectre.Console;
|
||||
using YaeAchievement.Outputs;
|
||||
using YaeAchievement.Parsers;
|
||||
using YaeAchievement.res;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Export {
|
||||
|
||||
public static uint ExportTo { get; set; } = uint.MaxValue;
|
||||
public static int ExportTo { get; set; } = 114514;
|
||||
|
||||
public static void Choose(AchievementAllDataNotify data) {
|
||||
if (ExportTo == uint.MaxValue) {
|
||||
Console.Write(App.ExportChoose);
|
||||
while (Console.KeyAvailable) {
|
||||
Console.ReadKey(false);
|
||||
}
|
||||
if (!uint.TryParse(Console.ReadLine(), out var num)) num = 0;
|
||||
ExportTo = num;
|
||||
var targets = new Dictionary<string, Action<AchievementAllDataNotify>> {
|
||||
{ App.ExportTargetCocogoat, ToCocogoat },
|
||||
{ App.ExportTargetHuTao, ToHuTao },
|
||||
{ App.ExportTargetPaimon, ToPaimon },
|
||||
{ App.ExportTargetSeelie, ToSeelie },
|
||||
{ App.ExportTargetCsv, ToCSV },
|
||||
{ App.ExportTargetXunkong, ToXunkong },
|
||||
// { App.ExportTargetWxApp1, ToWxApp1 },
|
||||
{ App.ExportTargetTeyvatGuide, ToTeyvatGuide },
|
||||
{ App.ExportTargetUIAFJson, ToUIAFJson },
|
||||
// { "", ToRawJson }
|
||||
};
|
||||
Action<AchievementAllDataNotify> action;
|
||||
if (ExportTo == 114514) {
|
||||
var prompt = new SelectionPromptCompat<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
|
||||
action = targets[prompt.Prompt()];
|
||||
} else {
|
||||
action = targets.ElementAtOrDefault(ExportTo).Value ?? ToCocogoat;
|
||||
}
|
||||
((Action<AchievementAllDataNotify>) (ExportTo switch {
|
||||
1 => ToHuTao,
|
||||
2 => ToPaimon,
|
||||
3 => ToSeelie,
|
||||
4 => ToCSV,
|
||||
5 => ToXunkong,
|
||||
6 => ToWxApp1,
|
||||
7 => ToTeyvatGuide,
|
||||
8 => ToUIAFJson,
|
||||
9 => ToRawJson,
|
||||
_ => ToCocogoat
|
||||
})).Invoke(data);
|
||||
action(data);
|
||||
}
|
||||
|
||||
private static void ToCocogoat(AchievementAllDataNotify data) {
|
||||
@@ -46,16 +48,17 @@ 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) {
|
||||
Console.WriteLine(App.ExportToCocogoatFail);
|
||||
AnsiConsole.WriteLine(App.ExportToCocogoatFail, response.StatusCode);
|
||||
return;
|
||||
}
|
||||
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
var responseJson = JsonSerializer.Deserialize(responseText, CocogoatResponseContext.Default.CocogoatResponse)!;
|
||||
var cocogoatUrl = $"https://cocogoat.work/achievement?memo={responseJson.Key}";
|
||||
Console.WriteLine(cocogoatUrl);
|
||||
Utils.SetQuickEditMode(true);
|
||||
AnsiConsole.MarkupLineInterpolated($"[link]{cocogoatUrl}[/]");
|
||||
if (Utils.ShellOpen(cocogoatUrl))
|
||||
{
|
||||
Console.WriteLine(App.ExportToCocogoatSuccess);
|
||||
AnsiConsole.WriteLine(App.ExportToCocogoatSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,16 +70,16 @@ public static class Export {
|
||||
request.RequestUri = new Uri("https://api.qyinter.com/achievementRedis");
|
||||
request.Content = new StringContent(result, Encoding.UTF8, "application/json");
|
||||
using var response = Utils.CHttpClient.Send(request);
|
||||
Console.WriteLine(App.ExportToWxApp1Success, id);
|
||||
AnsiConsole.WriteLine(App.ExportToWxApp1Success, id);
|
||||
}
|
||||
|
||||
private static void ToHuTao(AchievementAllDataNotify data) {
|
||||
if (CheckWinUIAppScheme("hutao")) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("hutao://achievement/import");
|
||||
Console.WriteLine(App.ExportToSnapGenshinSuccess);
|
||||
AnsiConsole.WriteLine(App.ExportToSnapGenshinSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToSnapGenshinNeedUpdate);
|
||||
AnsiConsole.WriteLine(App.ExportToSnapGenshinNeedUpdate);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52");
|
||||
}
|
||||
}
|
||||
@@ -85,9 +88,9 @@ public static class Export {
|
||||
if (CheckWinUIAppScheme("xunkong")) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
|
||||
Console.WriteLine(App.ExportToXunkongSuccess);
|
||||
AnsiConsole.WriteLine(App.ExportToXunkongSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToXunkongNeedUpdate);
|
||||
AnsiConsole.WriteLine(App.ExportToXunkongNeedUpdate);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
|
||||
}
|
||||
}
|
||||
@@ -95,10 +98,10 @@ public static class Export {
|
||||
private static void ToTeyvatGuide(AchievementAllDataNotify data) {
|
||||
if (Process.GetProcessesByName("TeyvatGuide").Length != 0) {
|
||||
Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
|
||||
Utils.ShellOpen("teyvatguide://import_uigf?app=YaeAchievement");
|
||||
Console.WriteLine(App.ExportToTauriSuccess);
|
||||
Utils.ShellOpen("teyvatguide://import_uiaf?app=Yae");
|
||||
AnsiConsole.WriteLine(App.ExportToTauriSuccess);
|
||||
} else {
|
||||
Console.WriteLine(App.ExportToTauriFail);
|
||||
AnsiConsole.WriteLine(App.ExportToTauriFail);
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN");
|
||||
}
|
||||
}
|
||||
@@ -107,21 +110,21 @@ public static class Export {
|
||||
private static void ToUIAFJson(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json");
|
||||
if (TryWriteToFile(path, UIAFSerializer.Serialize(data))) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToPaimon(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-paimon.json");
|
||||
if (TryWriteToFile(path, PaimonSerializer.Serialize(data))) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToSeelie(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-seelie.json");
|
||||
if (TryWriteToFile(path, SeelieSerializer.Serialize(data))) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +135,7 @@ public static class Export {
|
||||
foreach (var ach in data.AchievementList.OrderBy(a => a.Id)) {
|
||||
if (UnusedAchievement.Contains(ach.Id)) continue;
|
||||
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
|
||||
Console.WriteLine($@"Unable to find {ach.Id} in metadata.");
|
||||
AnsiConsole.WriteLine($@"Unable to find {ach.Id} in metadata.");
|
||||
continue;
|
||||
}
|
||||
var finishAt = "";
|
||||
@@ -155,7 +158,7 @@ public static class Export {
|
||||
}));
|
||||
var path = Path.GetFullPath($"achievement-{DateTime.Now:yyyyMMddHHmmss}.csv");
|
||||
if (TryWriteToFile(path, $"\uFEFF{string.Join("\n", output)}")) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
|
||||
Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}");
|
||||
}
|
||||
}
|
||||
@@ -164,7 +167,7 @@ public static class Export {
|
||||
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
|
||||
var text = AchievementRawDataSerializer.Serialize(data);
|
||||
if (TryWriteToFile(path, text)) {
|
||||
Console.WriteLine(App.ExportToFileSuccess, path);
|
||||
AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +194,7 @@ public static class Export {
|
||||
|
||||
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine($"{msg}: {ex.Message}");
|
||||
AnsiConsole.WriteLine($"{msg}: {ex.Message}");
|
||||
return ex.NativeErrorCode;
|
||||
}
|
||||
|
||||
@@ -200,13 +203,13 @@ public static class Export {
|
||||
File.WriteAllText(path, contents);
|
||||
return true;
|
||||
} catch (UnauthorizedAccessException) {
|
||||
Console.WriteLine(App.NoWritePermission, path);
|
||||
AnsiConsole.WriteLine(App.NoWritePermission, path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WxApp1Root {
|
||||
public sealed class WxApp1Root {
|
||||
|
||||
public string Key { get; init; } = null!;
|
||||
|
||||
@@ -219,7 +222,7 @@ public class WxApp1Root {
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class WxApp1Serializer : JsonSerializerContext {
|
||||
public sealed partial class WxApp1Serializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf, string key) => JsonSerializer.Serialize(new WxApp1Root {
|
||||
Key = key,
|
||||
@@ -227,8 +230,8 @@ public partial class WxApp1Serializer : JsonSerializerContext {
|
||||
}, Default.WxApp1Root);
|
||||
}
|
||||
|
||||
public record CocogoatResponse(string Key);
|
||||
public sealed record CocogoatResponse(string Key);
|
||||
|
||||
[JsonSerializable(typeof(CocogoatResponse))]
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
public partial class CocogoatResponseContext : JsonSerializerContext;
|
||||
public sealed partial class CocogoatResponseContext : JsonSerializerContext;
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public static class Extensions {
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<MD5> md5 = new (MD5.Create);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
|
||||
|
||||
public static byte[] ToBytes(this string text) {
|
||||
return Encoding.UTF8.GetBytes(text);
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this string text) {
|
||||
return text.ToBytes().MD5Hash();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this byte[] data) {
|
||||
return md5.Value.ComputeHash(data).ToHex().ToLower();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string SHA1Hash(this string text, bool base64 = true) {
|
||||
var bytes = sha1.Value.ComputeHash(text.ToBytes());
|
||||
return base64 ? bytes.ToBase64() : bytes.ToHex();
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] bytes) {
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
|
||||
public static string ToBase64(this byte[] bytes) {
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
global using System.Diagnostics;
|
||||
global using YaeAchievement.res;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Proto;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable ConvertToConstant.Global
|
||||
// ReSharper disable FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
|
||||
public static class GlobalVars {
|
||||
|
||||
public static bool PauseOnExit { get; set; } = true;
|
||||
@@ -21,14 +18,10 @@ public static class GlobalVars {
|
||||
public static readonly string CachePath = Path.Combine(DataPath, "cache");
|
||||
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
|
||||
|
||||
public const uint AppVersionCode = 235;
|
||||
public const string AppVersionName = "5.3";
|
||||
public const uint AppVersionCode = 243;
|
||||
public const string AppVersionName = "5.7.3";
|
||||
|
||||
public const string PipeName = "YaeAchievementPipe";
|
||||
public const string RinBucketHost = "https://rin.holohat.work";
|
||||
public const string SakuraBucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
|
||||
|
||||
public static CacheFile AchievementDataCache { get; } = new ("achievement_data");
|
||||
|
||||
[field:MaybeNull]
|
||||
public static AchievementInfo AchievementInfo =>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Memory;
|
||||
using Windows.Win32.System.Threading;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Injector {
|
||||
|
||||
public static unsafe bool CreateProcess(string path, out HANDLE hProc, out HANDLE hThread, out uint pid) {
|
||||
Span<char> cmdLines = stackalloc char[1]; // "\0"
|
||||
var si = new STARTUPINFOW {
|
||||
cb = unchecked((uint)sizeof(STARTUPINFOW))
|
||||
};
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
var result = Native.CreateProcess(
|
||||
path, ref cmdLines, default, default, false,
|
||||
PROCESS_CREATION_FLAGS.CREATE_SUSPENDED, default, dir, in si, out var pi
|
||||
);
|
||||
pid = pi.dwProcessId;
|
||||
hProc = pi.hProcess;
|
||||
hThread = pi.hThread;
|
||||
return result;
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
public static unsafe int LoadLibraryAndInject(HANDLE hProc, ReadOnlySpan<char> libPath) {
|
||||
fixed (char* lpModelName = "kernel32.dll") {
|
||||
var hKernel = Native.GetModuleHandle(lpModelName);
|
||||
if (hKernel.IsNull) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetModuleHandle fail");
|
||||
}
|
||||
fixed(byte* lpProcName = "LoadLibraryW"u8) {
|
||||
var pLoadLibrary = Native.GetProcAddress(hKernel, (PCSTR)lpProcName);
|
||||
if (pLoadLibrary.IsNull) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetProcAddress fail");
|
||||
}
|
||||
var libPathByteLen = (uint) libPath.Length * 2;
|
||||
var pBase = Native.VirtualAllocEx(hProc, default, libPathByteLen + 2, VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
|
||||
if ((nint)pBase == 0) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("VirtualAllocEx fail");
|
||||
}
|
||||
fixed (void* lpBuffer = libPath) {
|
||||
if (!Native.WriteProcessMemory(hProc, pBase, lpBuffer, libPathByteLen)) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("WriteProcessMemory fail");
|
||||
}
|
||||
}
|
||||
var lpStartAddress = (delegate* unmanaged[Stdcall]<void*, uint>)pLoadLibrary.Value; //THREAD_START_ROUTINE
|
||||
var hThread = Native.CreateRemoteThread(hProc, default, 0, lpStartAddress, pBase, 0);
|
||||
if (hThread.IsNull) {
|
||||
var e = new Win32Exception();
|
||||
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
|
||||
return e.PrintMsgAndReturnErrCode("CreateRemoteThread fail");
|
||||
}
|
||||
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
|
||||
Native.VirtualFreeEx(hProc, pBase, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
|
||||
}
|
||||
return !Native.CloseHandle(hThread) ? new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail") : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace YaeAchievement.Outputs;
|
||||
// ReSharper disable PropertyCanBeMadeInitOnly.Global
|
||||
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
|
||||
|
||||
public class PaimonRoot {
|
||||
public sealed class PaimonRoot {
|
||||
|
||||
public Dictionary<uint, Dictionary<uint, bool>> Achievement { get; set; } = null!;
|
||||
|
||||
@@ -29,7 +29,7 @@ public class PaimonRoot {
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class PaimonSerializer : JsonSerializerContext {
|
||||
public sealed partial class PaimonSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.PaimonRoot.FromNotify(ntf), Default.PaimonRoot);
|
||||
|
||||
@@ -8,9 +8,9 @@ namespace YaeAchievement.Outputs;
|
||||
// ReSharper disable PropertyCanBeMadeInitOnly.Global
|
||||
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
|
||||
|
||||
public class SeelieRoot {
|
||||
public sealed class SeelieRoot {
|
||||
|
||||
public class AchievementFinishStatus {
|
||||
public sealed class AchievementFinishStatus {
|
||||
|
||||
public bool Done => true;
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SeelieRoot {
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class SeelieSerializer : JsonSerializerContext {
|
||||
public sealed partial class SeelieSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.SeelieRoot.FromNotify(ntf), Default.SeelieRoot);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace YaeAchievement.Outputs;
|
||||
// ReSharper disable PropertyCanBeMadeInitOnly.Global
|
||||
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
|
||||
|
||||
public class UApplicationInfo {
|
||||
public sealed class UApplicationInfo {
|
||||
|
||||
public string ExportApp => "YaeAchievement";
|
||||
|
||||
@@ -21,7 +21,7 @@ public class UApplicationInfo {
|
||||
|
||||
}
|
||||
|
||||
public class UAchievementInfo {
|
||||
public sealed class UAchievementInfo {
|
||||
|
||||
public uint Id { get; set; }
|
||||
|
||||
@@ -33,7 +33,7 @@ public class UAchievementInfo {
|
||||
|
||||
}
|
||||
|
||||
public class UIAFRoot {
|
||||
public sealed class UIAFRoot {
|
||||
|
||||
public UApplicationInfo Info => new ();
|
||||
|
||||
@@ -57,7 +57,7 @@ public class UIAFRoot {
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class UIAFSerializer : JsonSerializerContext {
|
||||
public sealed partial class UIAFSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Google.Protobuf;
|
||||
using YaeAchievement.res;
|
||||
using Spectre.Console;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement.Parsers;
|
||||
|
||||
@@ -12,7 +13,7 @@ public enum AchievementStatus {
|
||||
RewardTaken,
|
||||
}
|
||||
|
||||
public class AchievementItem {
|
||||
public sealed class AchievementItem {
|
||||
|
||||
public uint Id { get; init; }
|
||||
public uint TotalProgress { get; init; }
|
||||
@@ -22,15 +23,15 @@ public class AchievementItem {
|
||||
|
||||
}
|
||||
|
||||
public class AchievementAllDataNotify {
|
||||
public sealed class AchievementAllDataNotify {
|
||||
|
||||
public List<AchievementItem> AchievementList { get; private init; } = [];
|
||||
|
||||
private static AchievementAllDataNotify? Instance { get; set; }
|
||||
|
||||
public static bool OnReceive(BinaryReader reader) {
|
||||
var bytes = reader.ReadBytes(reader.ReadInt32());
|
||||
GlobalVars.AchievementDataCache.Write(bytes);
|
||||
var bytes = reader.ReadBytes();
|
||||
CacheFile.Write("achievement_data", bytes);
|
||||
Instance = ParseFrom(bytes);
|
||||
return true;
|
||||
}
|
||||
@@ -60,7 +61,7 @@ public class AchievementAllDataNotify {
|
||||
}
|
||||
dict[tag >> 3] = eStream.ReadUInt32();
|
||||
}
|
||||
if (dict != null) {
|
||||
if (dict is { Count: > 2 }) { // at least 3 fields
|
||||
data.Add(dict);
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
@@ -72,7 +73,7 @@ public class AchievementAllDataNotify {
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine("Parse failed");
|
||||
AnsiConsole.WriteLine("Parse failed");
|
||||
File.WriteAllBytes("achievement_raw_data.bin", bytes);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
@@ -80,7 +81,14 @@ public class AchievementAllDataNotify {
|
||||
return new AchievementAllDataNotify();
|
||||
}
|
||||
uint tId, sId, iId, currentId, totalId;
|
||||
if (data.Count > 20) { /* uwu */
|
||||
if (data.All(CheckKnownFieldIdIsValid)) {
|
||||
var info = GlobalVars.AchievementInfo.PbInfo;
|
||||
iId = info.Id;
|
||||
tId = info.FinishTimestamp;
|
||||
sId = info.Status;
|
||||
totalId = info.TotalProgress;
|
||||
currentId = info.CurrentProgress;
|
||||
} else if (data.Count > 20) {
|
||||
(tId, var cnt) = data // ↓ 2020-09-15 04:15:14
|
||||
.GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2);
|
||||
sId = data // FINISHED ↓ ↓ REWARD_TAKEN
|
||||
@@ -95,21 +103,14 @@ public class AchievementAllDataNotify {
|
||||
.Select(g => (FieldIds: g.Key, Count: g.Count()))
|
||||
.MaxBy(p => p.Count)
|
||||
.FieldIds;
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
|
||||
#endif
|
||||
AnsiConsole.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
|
||||
#endif
|
||||
} else {
|
||||
var info = GlobalVars.AchievementInfo.PbInfo; // ...
|
||||
iId = info.Id;
|
||||
tId = info.FinishTimestamp;
|
||||
sId = info.Status;
|
||||
totalId = info.TotalProgress;
|
||||
currentId = info.CurrentProgress;
|
||||
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
|
||||
Console.WriteLine(App.WaitMetadataUpdate);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
|
||||
Environment.Exit(0);
|
||||
return null!;
|
||||
}
|
||||
return new AchievementAllDataNotify {
|
||||
AchievementList = data.Select(dict => new AchievementItem {
|
||||
@@ -120,6 +121,18 @@ public class AchievementAllDataNotify {
|
||||
FinishTimestamp = dict.GetValueOrDefault(tId),
|
||||
}).ToList()
|
||||
};
|
||||
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
||||
static bool CheckKnownFieldIdIsValid(Dictionary<uint, uint> data) {
|
||||
var info = GlobalVars.AchievementInfo;
|
||||
var status = data.GetValueOrDefault(info.PbInfo.Status, 114514u);
|
||||
if (status is 0 or > 3) {
|
||||
return false;
|
||||
}
|
||||
if (status > 1 && data.GetValueOrDefault(info.PbInfo.FinishTimestamp) < 1600114514) { // 2020-09-15 04:15:14
|
||||
return false;
|
||||
}
|
||||
return info.Items.ContainsKey(data.GetValueOrDefault(info.PbInfo.Id));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -130,7 +143,7 @@ public class AchievementAllDataNotify {
|
||||
GenerationMode = JsonSourceGenerationMode.Serialization,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
|
||||
)]
|
||||
public partial class AchievementRawDataSerializer : JsonSerializerContext {
|
||||
public sealed partial class AchievementRawDataSerializer : JsonSerializerContext {
|
||||
|
||||
public static string Serialize(AchievementAllDataNotify ntf) {
|
||||
return JsonSerializer.Serialize(ntf, Default.AchievementAllDataNotify);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Google.Protobuf;
|
||||
using Proto;
|
||||
using Spectre.Console;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
// ReSharper disable CollectionNeverQueried.Global
|
||||
@@ -8,7 +9,7 @@ using Proto;
|
||||
|
||||
namespace YaeAchievement.Parsers;
|
||||
|
||||
public class PlayerStoreNotify {
|
||||
public sealed class PlayerStoreNotify {
|
||||
|
||||
public uint WeightLimit { get; set; }
|
||||
|
||||
@@ -19,7 +20,7 @@ public class PlayerStoreNotify {
|
||||
public static PlayerStoreNotify Instance { get; } = new ();
|
||||
|
||||
public static bool OnReceive(BinaryReader reader) {
|
||||
var bytes = reader.ReadBytes(reader.ReadInt32());
|
||||
var bytes = reader.ReadBytes();
|
||||
Instance.ParseFrom(bytes);
|
||||
return true;
|
||||
}
|
||||
@@ -51,7 +52,7 @@ public class PlayerStoreNotify {
|
||||
}
|
||||
} catch (InvalidProtocolBufferException) {
|
||||
// ReSharper disable once LocalizableElement
|
||||
Console.WriteLine("Parse failed");
|
||||
AnsiConsole.WriteLine("Parse failed");
|
||||
File.WriteAllBytes("store_raw_data.bin", bytes);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Console;
|
||||
using Spectre.Console;
|
||||
using YaeAchievement.Parsers;
|
||||
using YaeAchievement.res;
|
||||
using YaeAchievement.Utilities;
|
||||
using static YaeAchievement.Utils;
|
||||
|
||||
namespace YaeAchievement;
|
||||
@@ -13,47 +11,68 @@ internal static class Program {
|
||||
|
||||
public static async Task Main(string[] args) {
|
||||
|
||||
AnsiConsole.WriteLine(@"----------------------------------------------------");
|
||||
AnsiConsole.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
|
||||
AnsiConsole.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
|
||||
AnsiConsole.WriteLine(@"----------------------------------------------------");
|
||||
|
||||
if (!new Mutex(true, @"Global\YaeMiku~uwu").WaitOne(0, false)) {
|
||||
Console.WriteLine(App.AnotherInstance);
|
||||
AnsiConsole.WriteLine(App.AnotherInstance);
|
||||
Environment.Exit(302);
|
||||
}
|
||||
|
||||
SentrySdk.Init(options => {
|
||||
options.Dsn = "https://92f11b64b0ef52cabc94f21df0428f5b@sentry.snapgenshin.com/9";
|
||||
#if DEBUG
|
||||
options.Debug = true;
|
||||
#endif
|
||||
options.TracesSampleRate = 1.0;
|
||||
options.AutoSessionTracking = true;
|
||||
options.SetBeforeSend(static e => {
|
||||
e.Release = GlobalVars.AppVersionName;
|
||||
return e;
|
||||
});
|
||||
options.SetBeforeSendTransaction(static e => {
|
||||
e.Release = GlobalVars.AppVersionName;
|
||||
return e;
|
||||
});
|
||||
options.CacheDirectoryPath = GlobalVars.DataPath;
|
||||
});
|
||||
InstallExitHook();
|
||||
InstallExceptionHook();
|
||||
|
||||
CheckGenshinIsRunning();
|
||||
if (GetGameProcess() != null) {
|
||||
AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
Console.WriteLine(@"----------------------------------------------------");
|
||||
Console.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
|
||||
Console.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
|
||||
Console.WriteLine(@"----------------------------------------------------");
|
||||
await CheckUpdate(ToBooleanOrDefault(args.ElementAtOrDefault(2)));
|
||||
|
||||
AppConfig.Load(args.GetOrNull(0) ?? "auto");
|
||||
Export.ExportTo = ToUIntOrNull(args.GetOrNull(1)) ?? uint.MaxValue;
|
||||
|
||||
await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2)));
|
||||
|
||||
var historyCache = GlobalVars.AchievementDataCache;
|
||||
AppConfig.Load(args.ElementAtOrDefault(0) ?? "auto");
|
||||
Export.ExportTo = ToIntOrDefault(args.ElementAtOrDefault(1), 114514);
|
||||
|
||||
AchievementAllDataNotify? data = null;
|
||||
try {
|
||||
data = AchievementAllDataNotify.ParseFrom(historyCache.Read().Content.ToByteArray());
|
||||
if (CacheFile.TryRead("achievement_data", out var cache)) {
|
||||
data = AchievementAllDataNotify.ParseFrom(cache.Content.ToByteArray());
|
||||
}
|
||||
} catch (Exception) { /* ignored */ }
|
||||
|
||||
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) {
|
||||
Console.WriteLine(App.UsePreviousData);
|
||||
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") {
|
||||
if (data != null && CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow) {
|
||||
var prompt = new SelectionPromptCompat<string>()
|
||||
.Title(App.UsePreviousData)
|
||||
.AddChoices(App.CommonYes, App.CommonNo);
|
||||
if (prompt.Prompt() == App.CommonYes) {
|
||||
Export.Choose(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StartAndWaitResult(AppConfig.GamePath, new Dictionary<byte, Func<BinaryReader, bool>> {
|
||||
StartAndWaitResult(AppConfig.GamePath, new Dictionary<int, Func<BinaryReader, bool>> {
|
||||
{ 1, AchievementAllDataNotify.OnReceive },
|
||||
{ 2, PlayerStoreNotify.OnReceive },
|
||||
{ 100, PlayerPropNotify.OnReceive },
|
||||
{ 3, PlayerPropNotify.OnReceive },
|
||||
}, () => {
|
||||
#if DEBUG
|
||||
#if DEBUG_EX
|
||||
PlayerPropNotify.OnFinish();
|
||||
File.WriteAllText("store_data.json", JsonSerializer.Serialize(PlayerStoreNotify.Instance, new JsonSerializerOptions {
|
||||
WriteIndented = true,
|
||||
@@ -61,16 +80,16 @@ internal static class Program {
|
||||
}));
|
||||
#endif
|
||||
AchievementAllDataNotify.OnFinish();
|
||||
Environment.Exit(0);
|
||||
});
|
||||
while (true) {}
|
||||
}
|
||||
|
||||
[ModuleInitializer]
|
||||
internal static unsafe void SetupConsole() {
|
||||
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
|
||||
CONSOLE_MODE mode = default;
|
||||
Native.GetConsoleMode(handle, &mode);
|
||||
Native.SetConsoleMode(handle, mode & ~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE);
|
||||
internal static void SetupConsole() {
|
||||
SetQuickEditMode(false);
|
||||
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
|
||||
FixTerminalFont();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
using System.IO.Compression;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Google.Protobuf;
|
||||
using Proto;
|
||||
|
||||
namespace YaeAchievement.Utilities;
|
||||
|
||||
public class CacheFile(string identifier) {
|
||||
public static class CacheFile {
|
||||
|
||||
private readonly string _cacheName = Path.Combine(GlobalVars.CachePath, $"{identifier.MD5Hash()[..16]}.miko");
|
||||
private CacheItem? _content;
|
||||
|
||||
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch;
|
||||
|
||||
public bool Exists() => File.Exists(_cacheName);
|
||||
|
||||
public CacheItem Read() {
|
||||
if (_content == null) {
|
||||
using var fInput = File.OpenRead(_cacheName);
|
||||
using var dInput = new GZipStream(fInput, CompressionMode.Decompress);
|
||||
_content = CacheItem.Parser.ParseFrom(dInput);
|
||||
static CacheFile() {
|
||||
// remove deprecated cache
|
||||
foreach (var file in Directory.EnumerateFiles(GlobalVars.CachePath, "*.miko")) {
|
||||
File.Delete(file);
|
||||
}
|
||||
return _content;
|
||||
}
|
||||
|
||||
public void Write(string data, string? etag = null) => Write(ByteString.CopyFromUtf8(data), data.MD5Hash(), etag);
|
||||
public static DateTime GetLastWriteTime(string id) {
|
||||
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
|
||||
return File.Exists(fileName) ? File.GetLastWriteTimeUtc(fileName) : DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
public void Write(byte[] data, string? etag = null) => Write(ByteString.CopyFrom(data), data.MD5Hash(), etag);
|
||||
public static bool TryRead(string id, [NotNullWhen(true)] out CacheItem? item) {
|
||||
item = null;
|
||||
try {
|
||||
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
|
||||
using var fileStream = File.OpenRead(fileName);
|
||||
using var zipStream = new GZipStream(fileStream, CompressionMode.Decompress);
|
||||
item = CacheItem.Parser.ParseFrom(zipStream);
|
||||
return true;
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Write(ByteString data, string hash, string? etag) {
|
||||
using var fOut = File.OpenWrite(_cacheName);
|
||||
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize);
|
||||
public static void Write(string id, byte[] data, string? etag = null) {
|
||||
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
|
||||
using var fileStream = File.Open(fileName, FileMode.Create);
|
||||
using var zipStream = new GZipStream(fileStream, CompressionLevel.SmallestSize);
|
||||
new CacheItem {
|
||||
Etag = etag ?? string.Empty,
|
||||
Version = 3,
|
||||
Checksum = hash,
|
||||
Content = data
|
||||
}.WriteTo(cOut);
|
||||
Checksum = GetBinHash(data),
|
||||
Content = ByteString.CopyFrom(data)
|
||||
}.WriteTo(zipStream);
|
||||
}
|
||||
|
||||
private static string GetStrHash(string value) => GetBinHash(Encoding.UTF8.GetBytes(value));
|
||||
|
||||
private static string GetBinHash(byte[] value) => Convert.ToHexStringLower(MD5.HashData(value));
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
public static class Collection {
|
||||
|
||||
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary,
|
||||
params TKey[] keys
|
||||
) {
|
||||
foreach (var key in keys) {
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Linq;
|
||||
namespace System.Collections.Generic {
|
||||
|
||||
public static class Enumerable {
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class CollectionExtensions {
|
||||
|
||||
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
|
||||
this IEnumerable<Dictionary<TKey, TValue>> source,
|
||||
Func<TValue, bool> condition
|
||||
) where TKey : notnull {
|
||||
return source.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key)).GroupBy(x => x);
|
||||
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dictionary, params TKey[] keys
|
||||
) {
|
||||
foreach (var key in keys) {
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
namespace System.Linq {
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class EnumerableExtensions {
|
||||
|
||||
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
|
||||
this IEnumerable<Dictionary<TKey, TValue>> source,
|
||||
Func<TValue, bool> condition
|
||||
) where TKey : notnull => source
|
||||
.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key))
|
||||
.GroupBy(x => x);
|
||||
}
|
||||
}
|
||||
|
||||
24
YaeAchievement/src/Utilities/Extensions/Process.cs
Normal file
24
YaeAchievement/src/Utilities/Extensions/Process.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using static Windows.Win32.System.Threading.PROCESS_ACCESS_RIGHTS;
|
||||
|
||||
// ReSharper disable CheckNamespace
|
||||
|
||||
namespace System.Diagnostics;
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class ProcessExtensions {
|
||||
|
||||
public static unsafe string? GetFileName(this Process process) {
|
||||
using var hProc = Native.OpenProcess_SafeHandle(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, (uint) process.Id);
|
||||
if (hProc.IsInvalid) {
|
||||
return null;
|
||||
}
|
||||
var sProcPath = stackalloc char[32767];
|
||||
return Native.GetModuleFileNameEx((HANDLE) hProc.DangerousGetHandle(), HMODULE.Null, sProcPath, 32767) == 0
|
||||
? null
|
||||
: new string(sProcPath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,32 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Spectre.Console;
|
||||
|
||||
// ReSharper disable CheckNamespace
|
||||
|
||||
namespace Google.Protobuf;
|
||||
|
||||
public static class CodedInputStreamExtensions {
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class BinaryReaderExtensions {
|
||||
|
||||
public static byte[] ReadBytes(this BinaryReader reader) {
|
||||
try {
|
||||
var length = reader.ReadInt32();
|
||||
if (length is < 0 or > 114514 * 2) {
|
||||
throw new ArgumentException(nameof(length));
|
||||
}
|
||||
return reader.ReadBytes(length);
|
||||
} catch (Exception e) when (e is IOException or ArgumentException) {
|
||||
AnsiConsole.WriteLine(App.StreamReadDataFail);
|
||||
Environment.Exit(-1);
|
||||
throw new UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class CodedInputStreamExtensions {
|
||||
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Method)]
|
||||
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);
|
||||
|
||||
127
YaeAchievement/src/Utilities/GameProcess.cs
Normal file
127
YaeAchievement/src/Utilities/GameProcess.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||
using Windows.Win32.System.LibraryLoader;
|
||||
using Windows.Win32.System.Threading;
|
||||
using Spectre.Console;
|
||||
using static Windows.Win32.System.Memory.VIRTUAL_ALLOCATION_TYPE;
|
||||
using static Windows.Win32.System.Memory.PAGE_PROTECTION_FLAGS;
|
||||
using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
namespace YaeAchievement.Utilities;
|
||||
|
||||
internal sealed unsafe class GameProcess {
|
||||
|
||||
public uint Id { get; }
|
||||
|
||||
public HANDLE Handle { get; }
|
||||
|
||||
public HANDLE MainThreadHandle { get; }
|
||||
|
||||
public event Action? OnExit;
|
||||
|
||||
public GameProcess(string path) {
|
||||
const PROCESS_CREATION_FLAGS flags = PROCESS_CREATION_FLAGS.CREATE_SUSPENDED;
|
||||
Span<char> cmdLines = stackalloc char[1]; // "\0"
|
||||
var si = new STARTUPINFOW {
|
||||
cb = (uint) sizeof(STARTUPINFOW)
|
||||
};
|
||||
var wd = Path.GetDirectoryName(path)!;
|
||||
if (!Native.CreateProcess(path, ref cmdLines, null, null, false, flags, null, wd, si, out var pi)) {
|
||||
var argumentData = new Dictionary<string, object> {
|
||||
{ "path", path },
|
||||
{ "workdir", wd },
|
||||
{ "file_exists", File.Exists(path) },
|
||||
};
|
||||
throw new ApplicationException($"CreateProcess fail: {Marshal.GetLastPInvokeErrorMessage()}") {
|
||||
Data = {
|
||||
{ "sentry:context:Arguments", argumentData }
|
||||
}
|
||||
};
|
||||
}
|
||||
Id = pi.dwProcessId;
|
||||
Handle = pi.hProcess;
|
||||
MainThreadHandle = pi.hThread;
|
||||
Task.Run(() => {
|
||||
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
|
||||
OnExit?.Invoke();
|
||||
}).ContinueWith(task => { if (task.IsFaulted) Utils.OnUnhandledException(task.Exception!); });
|
||||
}
|
||||
|
||||
public void LoadLibrary(string libPath) {
|
||||
try {
|
||||
var hKrnl32 = NativeLibrary.Load("kernel32");
|
||||
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
|
||||
var libPathLen = (uint) libPath.Length * sizeof(char);
|
||||
var lpLibPath = Native.VirtualAllocEx(Handle, null, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
if (lpLibPath == null) {
|
||||
throw new Win32Exception { Data = { { "api", "VirtualAllocEx" } } };
|
||||
}
|
||||
fixed (void* lpBuffer = libPath) {
|
||||
if (!Native.WriteProcessMemory(Handle, lpLibPath, lpBuffer, libPathLen)) {
|
||||
throw new Win32Exception { Data = { { "api", "WriteProcessMemory" } } };
|
||||
}
|
||||
}
|
||||
var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE
|
||||
var hThread = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress, lpLibPath, 0);
|
||||
if (hThread.IsNull) {
|
||||
throw new Win32Exception { Data = { { "api", "CreateRemoteThread" } } };
|
||||
}
|
||||
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
|
||||
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
|
||||
}
|
||||
// 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) {
|
||||
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
|
||||
}
|
||||
var libMainProc = Native.GetProcAddress(libHandle, "YaeMain");
|
||||
if (libMainProc.IsNull) {
|
||||
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
|
||||
}
|
||||
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);
|
||||
if (hThread2.IsNull) {
|
||||
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };
|
||||
}
|
||||
Native.CloseHandle(hThread2);
|
||||
Native.CloseHandle(hThread);
|
||||
} catch (Win32Exception e) {
|
||||
_ = Terminate(0);
|
||||
AnsiConsole.WriteLine(App.LoadLibraryFail, e.Data["api"]!, e.NativeErrorCode, e.Message);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ResumeMainThread() => Native.ResumeThread(MainThreadHandle) != 0xFFFFFFFF;
|
||||
|
||||
public bool Terminate(uint exitCode) => Native.TerminateProcess(Handle, exitCode);
|
||||
|
||||
}
|
||||
47
YaeAchievement/src/Utilities/SelectionPromptCompat.cs
Normal file
47
YaeAchievement/src/Utilities/SelectionPromptCompat.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Spectre.Console;
|
||||
|
||||
namespace YaeAchievement.Utilities;
|
||||
|
||||
public sealed class SelectionPromptCompat<T> where T : notnull {
|
||||
|
||||
private readonly List<T> _choices = [];
|
||||
private readonly SelectionPrompt<T> _prompt = new ();
|
||||
|
||||
public SelectionPromptCompat<T> Title(string? title) {
|
||||
_prompt.Title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SelectionPromptCompat<T> AddChoices(params IEnumerable<T> choices) {
|
||||
foreach (var choice in choices) {
|
||||
_prompt.AddChoice(choice);
|
||||
_choices.Add(choice);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public T Prompt() {
|
||||
if (AnsiConsole.Profile.Capabilities.Ansi) {
|
||||
var title = _prompt.Title;
|
||||
_prompt.Title += $" ({App.SelectionPromptCompatAnsiTip})";
|
||||
var result = AnsiConsole.Prompt(_prompt);
|
||||
_prompt.Title = title;
|
||||
return result;
|
||||
}
|
||||
if (_prompt.Title != null) {
|
||||
AnsiConsole.WriteLine(_prompt.Title + $" ({App.SelectionPromptCompatNonAnsiTip})");
|
||||
}
|
||||
for (var i = 0; i < _choices.Count; i++) {
|
||||
var choice = _choices[i];
|
||||
AnsiConsole.WriteLine($"[{i}] {choice}");
|
||||
}
|
||||
var choosePrompt = new TextPrompt<int>(App.SelectionPromptCompatChooseOne).Validate(i => {
|
||||
if (i < 0 || i >= _choices.Count) {
|
||||
return ValidationResult.Error(string.Format(App.SelectionPromptCompatInvalidChoice, _choices.Count - 1));
|
||||
}
|
||||
return ValidationResult.Success();
|
||||
});
|
||||
var resultIndex = AnsiConsole.Prompt(choosePrompt);
|
||||
return _choices[resultIndex];
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Pipes;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Console;
|
||||
using Proto;
|
||||
using YaeAchievement.res;
|
||||
using Spectre.Console;
|
||||
using YaeAchievement.Utilities;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Utils {
|
||||
|
||||
public static readonly HttpClient CHttpClient = new (new HttpClientHandler {
|
||||
public static HttpClient CHttpClient { get; } = new (new SentryHttpMessageHandler(new HttpClientHandler {
|
||||
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
|
||||
}) {
|
||||
})) {
|
||||
DefaultRequestHeaders = {
|
||||
UserAgent = {
|
||||
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
|
||||
@@ -25,46 +28,92 @@ public static class Utils {
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<byte[]> GetBucketFile(string path, bool cache = true) {
|
||||
try {
|
||||
return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost));
|
||||
} catch (Exception e) when(e is SocketException or TaskCanceledException) {
|
||||
Console.WriteLine(App.NetworkError, e.Message);
|
||||
Environment.Exit(-1);
|
||||
return null!;
|
||||
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
|
||||
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
|
||||
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) {}
|
||||
}
|
||||
async Task<byte[]> GetFile(string host) {
|
||||
using var msg = new HttpRequestMessage();
|
||||
msg.Method = HttpMethod.Get;
|
||||
msg.RequestUri = new Uri($"{host}/{path}");
|
||||
var cacheFile = new CacheFile(path);
|
||||
if (cache && cacheFile.Exists()) {
|
||||
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}");
|
||||
//
|
||||
try {
|
||||
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();
|
||||
return data;
|
||||
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
|
||||
AnsiConsole.WriteLine(App.NetworkError, e.Message);
|
||||
}
|
||||
transaction.Finish();
|
||||
Environment.Exit(-1);
|
||||
throw new UnreachableException();
|
||||
static async Task<byte[]> GetFileFromCdn(string url, string? cacheKey, uint hash, uint size) {
|
||||
var data = await GetFileReal(url, cacheKey);
|
||||
if (data.Length != size || Crc32.Compute(data) != hash) {
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
using var response = await CHttpClient.SendAsync(msg);
|
||||
if (cache && response.StatusCode == HttpStatusCode.NotModified) {
|
||||
return cacheFile.Read().Content.ToByteArray();
|
||||
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;
|
||||
if (cacheKey != null && CacheFile.TryRead(cacheKey, out cache)) {
|
||||
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
|
||||
}
|
||||
using var response = await CHttpClient.SendAsync(reqwest);
|
||||
if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
|
||||
return cache.Content.ToByteArray();
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
var responseBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
if (cache) {
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
if (cacheKey != null) {
|
||||
var etag = response.Headers.ETag!.Tag;
|
||||
cacheFile.Write(responseBytes, etag);
|
||||
CacheFile.Write(cacheKey, bytes, etag);
|
||||
}
|
||||
return responseBytes;
|
||||
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) {
|
||||
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
|
||||
}
|
||||
|
||||
public static uint? ToUIntOrNull(string? value) {
|
||||
return value != null ? uint.TryParse(value, out var result) ? result : null : null;
|
||||
}
|
||||
|
||||
public static bool ToBooleanOrFalse(string? value) {
|
||||
return value != null && bool.TryParse(value, out var result) && result;
|
||||
public static bool ToBooleanOrDefault(string? value, bool defaultValue = false) {
|
||||
return value != null && bool.TryParse(value, out var result) ? result : defaultValue;
|
||||
}
|
||||
|
||||
public static unsafe void CopyToClipboard(string text) {
|
||||
@@ -83,161 +132,227 @@ public static class Utils {
|
||||
}
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private static UpdateInfo _updateInfo = null!;
|
||||
private static UpdateInfo? _updateInfo;
|
||||
|
||||
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
|
||||
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
|
||||
}
|
||||
|
||||
public static Task<T> StartSpinnerAsync<T>(string status, Func<StatusContext, Task<T>> func) {
|
||||
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
|
||||
}
|
||||
|
||||
public static async Task CheckUpdate(bool useLocalLib) {
|
||||
var info = UpdateInfo.Parser.ParseFrom(await GetBucketFile("schicksal/version"))!;
|
||||
if (GlobalVars.AppVersionCode < info.VersionCode) {
|
||||
Console.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, info.VersionName);
|
||||
Console.WriteLine(App.UpdateDescription, info.Description);
|
||||
if (info.EnableAutoUpdate) {
|
||||
Console.WriteLine(App.UpdateDownloading);
|
||||
var tmpPath = Path.GetTempFileName();
|
||||
await File.WriteAllBytesAsync(tmpPath, await GetBucketFile(info.PackageLink));
|
||||
var updaterArgs = $"{Environment.ProcessId}|{Environment.ProcessPath}|{tmpPath}";
|
||||
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
|
||||
await File.WriteAllBytesAsync(updaterPath, App.Updater);
|
||||
ShellOpen(updaterPath, updaterArgs.ToBytes().ToBase64());
|
||||
GlobalVars.PauseOnExit = false;
|
||||
Environment.Exit(0);
|
||||
try {
|
||||
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
|
||||
var versionInfo = _updateInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
|
||||
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
|
||||
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
|
||||
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
|
||||
if (versionInfo.EnableAutoUpdate) {
|
||||
var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink));
|
||||
var tmpPath = Path.GetTempFileName();
|
||||
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
|
||||
await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) {
|
||||
await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!;
|
||||
await srcStream.CopyToAsync(dstStream);
|
||||
}
|
||||
await File.WriteAllBytesAsync(tmpPath, newBin);
|
||||
ShellOpen(updaterPath, $"{Environment.ProcessId} \"{tmpPath}\"");
|
||||
await StartSpinnerAsync(App.UpdateChecking, _ => Task.Delay(1919810));
|
||||
GlobalVars.PauseOnExit = false;
|
||||
Environment.Exit(0);
|
||||
}
|
||||
AnsiConsole.MarkupLine($"[link]{App.DownloadLink}[/]", versionInfo.PackageLink);
|
||||
if (versionInfo.ForceUpdate) {
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
Console.WriteLine(App.DownloadLink, info.PackageLink);
|
||||
if (info.ForceUpdate) {
|
||||
Environment.Exit(0);
|
||||
if (versionInfo.EnableLibDownload && !useLocalLib) {
|
||||
var data = await GetBucketFile("schicksal/lic.dll");
|
||||
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
|
||||
}
|
||||
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
|
||||
// IO_SharingViolation_File
|
||||
// The process cannot access the file '{0}' because it is being used by another process.
|
||||
AnsiConsole.WriteLine("文件 {0} 被其它程序占用,请 重启电脑 或 解除文件占用 后重试。", e.Message[36..^46]);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
if (info.EnableLibDownload && !useLocalLib) {
|
||||
var data = await GetBucketFile("schicksal/lic.dll");
|
||||
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
|
||||
}
|
||||
_updateInfo = info;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||
public static bool ShellOpen(string path, string? args = null) {
|
||||
public static bool ShellOpen(string path, string args = "") {
|
||||
try {
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
if (args != null) {
|
||||
startInfo.Arguments = args;
|
||||
}
|
||||
return new Process {
|
||||
StartInfo = startInfo
|
||||
StartInfo = new ProcessStartInfo {
|
||||
FileName = path,
|
||||
UseShellExecute = true,
|
||||
Arguments = args
|
||||
}
|
||||
}.Start();
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void CheckGenshinIsRunning() {
|
||||
Process.EnterDebugMode();
|
||||
foreach (var process in Process.GetProcesses()) {
|
||||
if (process.ProcessName is "GenshinImpact" or "YuanShen"
|
||||
&& !process.HasExited
|
||||
&& process.MainWindowHandle != nint.Zero
|
||||
) {
|
||||
Console.WriteLine(App.GenshinIsRunning, process.Id);
|
||||
Environment.Exit(301);
|
||||
}
|
||||
}
|
||||
Process.LeaveDebugMode();
|
||||
}
|
||||
internal static Process? GetGameProcess() => Process.GetProcessesByName("YuanShen")
|
||||
.Concat(Process.GetProcessesByName("GenshinImpact"))
|
||||
.FirstOrDefault(p => File.Exists($"{p.GetFileName()}/../HoYoKProtect.sys"));
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static Process? proc;
|
||||
private static GameProcess? _proc;
|
||||
|
||||
public static void InstallExitHook() {
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
|
||||
proc?.Kill();
|
||||
_proc?.Terminate(0);
|
||||
if (GlobalVars.PauseOnExit) {
|
||||
Console.WriteLine(App.PressKeyToExit);
|
||||
AnsiConsole.WriteLine(App.PressKeyToExit);
|
||||
Console.ReadKey();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void InstallExceptionHook() {
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
|
||||
var ex = e.ExceptionObject;
|
||||
switch (ex) {
|
||||
case ApplicationException ex1:
|
||||
Console.WriteLine(ex1.Message);
|
||||
break;
|
||||
case SocketException ex2:
|
||||
Console.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
|
||||
break;
|
||||
case HttpRequestException ex3:
|
||||
Console.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine(ex.ToString());
|
||||
break;
|
||||
}
|
||||
Environment.Exit(-1);
|
||||
};
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) => OnUnhandledException((Exception) e.ExceptionObject);
|
||||
}
|
||||
|
||||
private static bool _isUnexpectedExit;
|
||||
|
||||
public static void OnUnhandledException(Exception ex) {
|
||||
SentrySdk.CaptureException(ex);
|
||||
switch (ex) {
|
||||
case ApplicationException ex1:
|
||||
AnsiConsole.WriteLine(ex1.Message);
|
||||
break;
|
||||
case SocketException ex2:
|
||||
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
|
||||
break;
|
||||
case HttpRequestException ex3:
|
||||
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
|
||||
break;
|
||||
default:
|
||||
AnsiConsole.WriteLine(ex.ToString());
|
||||
break;
|
||||
}
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
private static bool _isUnexpectedExit = true;
|
||||
|
||||
// ReSharper disable once UnusedMethodReturnValue.Global
|
||||
public static Thread StartAndWaitResult(string exePath, Dictionary<byte, Func<BinaryReader, bool>> handlers, Action onFinish) {
|
||||
if (!Injector.CreateProcess(exePath, out var hProcess, out var hThread, out var pid)) {
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("ICreateProcess fail"));
|
||||
public static void StartAndWaitResult(string exePath, Dictionary<int, Func<BinaryReader, bool>> handlers, Action onFinish) {
|
||||
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;
|
||||
}
|
||||
if (Injector.LoadLibraryAndInject(hProcess,GlobalVars.LibFilePath.AsSpan()) != 0)
|
||||
{
|
||||
if (!Native.TerminateProcess(hProcess, 0))
|
||||
{
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail"));
|
||||
}
|
||||
}
|
||||
Console.WriteLine(App.GameLoading, pid);
|
||||
proc = Process.GetProcessById(Convert.ToInt32(pid));
|
||||
proc.EnableRaisingEvents = true;
|
||||
proc.Exited += (_, _) => {
|
||||
if (_isUnexpectedExit)
|
||||
{
|
||||
proc = null;
|
||||
Console.WriteLine(App.GameProcessExit);
|
||||
Task.Run(() => {
|
||||
try {
|
||||
using var stream = new NamedPipeServerStream(GlobalVars.PipeName);
|
||||
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) {
|
||||
switch (type) {
|
||||
case 0xFC:
|
||||
writer.Write(nativeConf.AchievementCmdId);
|
||||
writer.Write(nativeConf.StoreCmdId);
|
||||
break;
|
||||
case 0xFD:
|
||||
writer.Write(methodRva.DoCmd);
|
||||
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;
|
||||
case 0xFE:
|
||||
_proc!.ResumeMainThread();
|
||||
break;
|
||||
case 0xFF:
|
||||
writer.Write(true);
|
||||
_isUnexpectedExit = false;
|
||||
onFinish();
|
||||
return;
|
||||
}
|
||||
if (handlers.TryGetValue(type, out var handler)) {
|
||||
if (handler(reader)) {
|
||||
handlers.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) when (e.Message == "Pipe is broken.") { } // SR.IO_PipeBroken
|
||||
}).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);
|
||||
}
|
||||
};
|
||||
if (Native.ResumeThread(hThread) == 0xFFFFFFFF)
|
||||
{
|
||||
var e = new Win32Exception();
|
||||
if (!Native.TerminateProcess(hProcess, 0))
|
||||
{
|
||||
new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail");
|
||||
}
|
||||
Environment.Exit(e.PrintMsgAndReturnErrCode("ResumeThread fail"));
|
||||
}
|
||||
if (!Native.CloseHandle(hProcess))
|
||||
{
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail"));
|
||||
}
|
||||
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
|
||||
}
|
||||
|
||||
var ts = new ThreadStart(() => {
|
||||
var server = new NamedPipeServerStream(GlobalVars.PipeName);
|
||||
server.WaitForConnection();
|
||||
using var reader = new BinaryReader(server);
|
||||
while (!proc.HasExited) {
|
||||
var type = reader.ReadByte();
|
||||
if (type == 0xFF) {
|
||||
_isUnexpectedExit = false;
|
||||
onFinish();
|
||||
break;
|
||||
}
|
||||
if (handlers.TryGetValue(type, out var handler)) {
|
||||
if (handler(reader)) {
|
||||
handlers.Remove(type);
|
||||
public static uint GetGameHash(string exePath) {
|
||||
try {
|
||||
Span<byte> buffer = stackalloc byte[0x10000];
|
||||
using var stream = File.OpenRead(exePath);
|
||||
_ = stream.ReadAtLeast(buffer, 0x10000, false);
|
||||
return Crc32.Compute(buffer);
|
||||
} catch (IOException) {
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
internal static unsafe void SetQuickEditMode(bool enable) {
|
||||
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
|
||||
CONSOLE_MODE mode = default;
|
||||
Native.GetConsoleMode(handle, &mode);
|
||||
mode = enable ? mode | CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE : mode &~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
|
||||
Native.SetConsoleMode(handle, mode);
|
||||
}
|
||||
|
||||
internal static unsafe void FixTerminalFont() {
|
||||
if (!CultureInfo.CurrentCulture.Name.StartsWith("zh")) {
|
||||
return;
|
||||
}
|
||||
var handle = Native.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE);
|
||||
var fontInfo = new CONSOLE_FONT_INFOEX {
|
||||
cbSize = (uint) sizeof(CONSOLE_FONT_INFOEX)
|
||||
};
|
||||
if (!Native.GetCurrentConsoleFontEx(handle, false, &fontInfo)) {
|
||||
return;
|
||||
}
|
||||
if (fontInfo.FaceName.ToString() == "Terminal") { // 点阵字体
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var th = new Thread(ts);
|
||||
th.Start();
|
||||
return th;
|
||||
}, 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!;
|
||||
}
|
||||
}
|
||||
|
||||
71
YaeAchievementLib/YaeAchievementLib.csproj
Normal file
71
YaeAchievementLib/YaeAchievementLib.csproj
Normal file
@@ -0,0 +1,71 @@
|
||||
<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="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>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="System.Diagnostics.CodeAnalysis" />
|
||||
</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>
|
||||
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
BIN
YaeAchievementLib/lib/libMinHook.x64.lib
Normal file
Binary file not shown.
217
YaeAchievementLib/src/Application.cs
Normal file
217
YaeAchievementLib/src/Application.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Yae.Utilities;
|
||||
using static Yae.GameMethod;
|
||||
|
||||
namespace Yae;
|
||||
|
||||
internal static unsafe class Application {
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
|
||||
private static uint Awake(nint hModule) {
|
||||
if (Interlocked.Exchange(ref _initialized, true)) {
|
||||
return 1;
|
||||
}
|
||||
Native.RegisterUnhandledExceptionHandler();
|
||||
Log.UseConsoleOutput();
|
||||
Log.Trace("~");
|
||||
Goshujin.Init();
|
||||
Goshujin.LoadCmdTable();
|
||||
Goshujin.LoadMethodTable();
|
||||
Goshujin.ResumeMainThread();
|
||||
//
|
||||
Native.WaitMainWindow();
|
||||
Log.ResetConsole();
|
||||
//
|
||||
RecordChecksum();
|
||||
MinHook.Attach(DoCmd, &OnDoCmd, out _doCmd);
|
||||
MinHook.Attach(ToUInt32, &OnToInt32, out _toInt32);
|
||||
MinHook.Attach(UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
|
||||
MinHook.Attach(EventSystemUpdate, &OnEventSystemUpdate, out _eventSystemUpdate);
|
||||
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
|
||||
|
||||
private static delegate*unmanaged<byte*, int, int> _toInt32;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static int OnToInt32(byte* val, int startIndex) {
|
||||
var ret = _toInt32(val, startIndex);
|
||||
if (startIndex != 6 || *(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 headPtr = val + 10;
|
||||
var dataLen = BinaryPrimitives.ReverseEndianness(*(uint*) (val + 6));
|
||||
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
|
||||
|
||||
#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
|
||||
};
|
||||
_ = 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
|
||||
|
||||
#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
|
||||
|
||||
}
|
||||
118
YaeAchievementLib/src/Goshujin.cs
Normal file
118
YaeAchievementLib/src/Goshujin.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
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<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 {
|
||||
|
||||
private static NamedPipeClientStream _pipeStream = null!;
|
||||
private static BinaryReader _pipeReader = null!;
|
||||
private static BinaryWriter _pipeWriter = null!;
|
||||
private static Lock _lock = null!;
|
||||
|
||||
public static void Init(string pipeName = "YaeAchievementPipe") {
|
||||
_lock = new Lock();
|
||||
_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) {
|
||||
using (_lock.EnterScope()) {
|
||||
_pipeWriter.Write((byte) 1);
|
||||
_pipeWriter.Write(data.Length);
|
||||
_pipeWriter.Write(data);
|
||||
_achievementDataPushed = true;
|
||||
ExitIfFinished();
|
||||
}
|
||||
}
|
||||
|
||||
public static void PushStoreData(Span<byte> data) {
|
||||
using (_lock.EnterScope()) {
|
||||
_pipeWriter.Write((byte) 2);
|
||||
_pipeWriter.Write(data.Length);
|
||||
_pipeWriter.Write(data);
|
||||
_storeDataPushed = true;
|
||||
ExitIfFinished();
|
||||
}
|
||||
}
|
||||
|
||||
public static void PushPlayerProp(int type, double value) {
|
||||
using (_lock.EnterScope()) {
|
||||
_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.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() {
|
||||
_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);
|
||||
_pipeReader.ReadBoolean();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
YaeAchievementLib/src/Utilities/Native.cs
Normal file
181
YaeAchievementLib/src/Utilities/Native.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint AsPointer(this ReadOnlySpan<byte> span) => *(nint*) Unsafe.AsPointer(ref span);
|
||||
|
||||
}
|
||||
|
||||
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")]
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
private static partial uint MinHookUninitialize();
|
||||
|
||||
static MinHook() {
|
||||
var result = MinHookInitialize();
|
||||
if (result != 0) {
|
||||
throw new InvalidOperationException($"Failed to initialize MinHook: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
74
YaeAchievementLib/src/Utilities/WinApi.cs
Normal file
74
YaeAchievementLib/src/Utilities/WinApi.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
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);
|
||||
|
||||
[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);
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
[LibraryImport("USER32.dll")]
|
||||
internal static partial nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
|
||||
|
||||
}
|
||||
4
lib/.gitignore
vendored
4
lib/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
.vs
|
||||
build
|
||||
YaeAchievementLib.vcxproj.user
|
||||
YaeAchievementLib.vcxproj.filters
|
||||
@@ -1,17 +0,0 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Yae.Lib</id>
|
||||
<version>5.3.1</version>
|
||||
<authors>HolographicHat</authors>
|
||||
<developmentDependency>true</developmentDependency>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<license type="expression">GPL-3.0-only</license>
|
||||
<licenseUrl>https://licenses.nuget.org/GPL-3.0-only</licenseUrl>
|
||||
<projectUrl>https://github.com/HolographicHat/Yae</projectUrl>
|
||||
<description>Yae Lib</description>
|
||||
<repository type="git" url="https://github.com/HolographicHat/Yae" commit="$commit$" />
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="build\x64\Release\YaeLib.dll" target="runtimes\win-x64\native" />
|
||||
</files>
|
||||
</package>
|
||||
@@ -1,29 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32407.343
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "YaeAchievementLib", "YaeAchievementLib.vcxproj", "{83C3DF1A-6219-408E-98A3-C7040CCC96FD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x64.Build.0 = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.ActiveCfg = Release|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x64.Build.0 = Release|x64
|
||||
{83C3DF1A-6219-408E-98A3-C7040CCC96FD}.Release|x86.ActiveCfg = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {470905A4-E6C4-4363-B44D-BAE9A50755A3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,114 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{83c3df1a-6219-408e-98a3-c7040ccc96fd}</ProjectGuid>
|
||||
<RootNamespace>YaeAchievementLib</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>YaeLib</TargetName>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
<TargetName>YaeLib</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>NotSet</SubSystem>
|
||||
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PostBuildEvent />
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>NotSet</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PostBuildEvent />
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\globals.h" />
|
||||
<ClInclude Include="src\il2cpp-types.h" />
|
||||
<ClInclude Include="src\il2cpp-init.h" />
|
||||
<ClInclude Include="src\NamedPipe.h" />
|
||||
<ClInclude Include="src\ntprivate.h" />
|
||||
<ClInclude Include="src\util.h" />
|
||||
<ClInclude Include="src\Zydis.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\dllmain.cpp" />
|
||||
<ClCompile Include="src\il2cpp-init.cpp" />
|
||||
<ClCompile Include="src\util.cpp" />
|
||||
<ClCompile Include="src\Zydis.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <span>
|
||||
|
||||
template <typename T>
|
||||
concept IsSpan = requires(T t) {
|
||||
{ t.data() } -> std::convertible_to<const void*>;
|
||||
{ t.size() } -> std::convertible_to<std::size_t>;
|
||||
{ t.size_bytes() } -> std::convertible_to<std::size_t>;
|
||||
};
|
||||
|
||||
class NamedPipe
|
||||
{
|
||||
HANDLE m_hPipe = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
NamedPipe(HANDLE hPipe) : m_hPipe(hPipe) {}
|
||||
~NamedPipe() { if (m_hPipe != INVALID_HANDLE_VALUE) CloseHandle(m_hPipe); }
|
||||
|
||||
operator HANDLE() const { return m_hPipe; }
|
||||
operator bool() const { return m_hPipe != INVALID_HANDLE_VALUE && m_hPipe != nullptr; }
|
||||
NamedPipe& operator= (HANDLE hPipe) {
|
||||
m_hPipe = hPipe;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Write(const void* data, size_t size) const
|
||||
{
|
||||
DWORD bytesWritten;
|
||||
if (!WriteFile(m_hPipe, data, static_cast<DWORD>(size), &bytesWritten, nullptr) || bytesWritten != size)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <IsSpan T>
|
||||
bool Write(const T& data) const
|
||||
{
|
||||
return Write(data.data(), data.size_bytes());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Write(const T& data) const
|
||||
{
|
||||
return Write(&data, sizeof(T));
|
||||
}
|
||||
|
||||
};
|
||||
54990
lib/src/Zydis.c
54990
lib/src/Zydis.c
File diff suppressed because one or more lines are too long
12113
lib/src/Zydis.h
12113
lib/src/Zydis.h
File diff suppressed because it is too large
Load Diff
@@ -1,274 +0,0 @@
|
||||
// ReSharper disable CppClangTidyCertErr33C
|
||||
#include <Windows.h>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <future>
|
||||
#include <TlHelp32.h>
|
||||
|
||||
#include "globals.h"
|
||||
#include "util.h"
|
||||
#include "il2cpp-init.h"
|
||||
#include "il2cpp-types.h"
|
||||
#include "ntprivate.h"
|
||||
|
||||
CRITICAL_SECTION CriticalSection;
|
||||
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index);
|
||||
|
||||
namespace
|
||||
{
|
||||
PacketType GetPacketType(const PacketMeta* packet)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto cmdid = packet->CmdId;
|
||||
|
||||
if (AchievementId && cmdid == AchievementId)
|
||||
return PacketType::Achivement;
|
||||
|
||||
if (AchievementIdSet.contains(cmdid) && packet->DataLength > 500)
|
||||
return PacketType::Achivement;
|
||||
|
||||
if (PlayerStoreId && cmdid == PlayerStoreId)
|
||||
return PacketType::Inventory;
|
||||
|
||||
return PacketType::None;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Hook {
|
||||
|
||||
|
||||
uint16_t __fastcall BitConverter_ToUInt16(Array<uint8_t>* val, const int startIndex)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto ToUInt16 = reinterpret_cast<decltype(&BitConverter_ToUInt16)>(Offset.BitConverter_ToUInt16);
|
||||
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint((HANDLE)-2, 0, false, 0);
|
||||
const auto ret = ToUInt16(val, startIndex);
|
||||
SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true, 0);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
|
||||
if (ret != 0xAB89)
|
||||
return ret;
|
||||
|
||||
const auto packet = val->As<PacketMeta*>();
|
||||
const auto packetType = GetPacketType(packet);
|
||||
if (packetType == PacketType::None)
|
||||
return ret;
|
||||
|
||||
#ifdef _DEBUG
|
||||
std::println("PacketType: {}", static_cast<uint8_t>(packetType));
|
||||
std::println("CmdId: {}", packet->CmdId);
|
||||
std::println("DataLength: {}", packet->DataLength);
|
||||
//std::println("Data: {}", Util::Base64Encode(packet->AsSpan()));
|
||||
#endif
|
||||
|
||||
if (!MessagePipe.Write(packetType))
|
||||
Util::Win32ErrorDialog(1002, GetLastError());
|
||||
|
||||
if (!MessagePipe.Write(packet->DataLength))
|
||||
Util::Win32ErrorDialog(1003, GetLastError());
|
||||
|
||||
if (!MessagePipe.Write(packet->AsSpan()))
|
||||
Util::Win32ErrorDialog(1004, GetLastError());
|
||||
|
||||
if (!AchievementsWritten)
|
||||
AchievementsWritten = packetType == PacketType::Achivement;
|
||||
|
||||
if (!PlayerStoreWritten)
|
||||
PlayerStoreWritten = packetType == PacketType::Inventory;
|
||||
|
||||
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::End))
|
||||
Util::Win32ErrorDialog(9001, GetLastError());
|
||||
#ifdef _DEBUG
|
||||
system("pause");
|
||||
#endif
|
||||
ExitProcess(0);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void __fastcall AccountDataItem_UpdateNormalProp(const void* __this, const int type, const double value, const double lastValue, const int state)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto UpdateNormalProp = reinterpret_cast<decltype(&AccountDataItem_UpdateNormalProp)>(Offset.AccountDataItem_UpdateNormalProp);
|
||||
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint((HANDLE)-2, 0, false, 1);
|
||||
UpdateNormalProp(__this, type, value, lastValue, state);
|
||||
SetBreakpoint((HANDLE)-2, Offset.AccountDataItem_UpdateNormalProp, true, 1);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
|
||||
#ifdef _DEBUG
|
||||
std::println("PropType: {}", type);
|
||||
std::println("PropState: {}", state);
|
||||
std::println("PropValue: {}", value);
|
||||
std::println("PropLastValue: {}", lastValue);
|
||||
#endif
|
||||
if (RequiredPlayerProperties.erase(type) != 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::PropData))
|
||||
Util::Win32ErrorDialog(2002, GetLastError());
|
||||
if (!MessagePipe.Write(type))
|
||||
Util::Win32ErrorDialog(2003, GetLastError());
|
||||
if (!MessagePipe.Write(value))
|
||||
Util::Win32ErrorDialog(2004, GetLastError());
|
||||
}
|
||||
|
||||
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
|
||||
{
|
||||
if (!MessagePipe.Write(PacketType::End))
|
||||
Util::Win32ErrorDialog(9001, GetLastError());
|
||||
#ifdef _DEBUG
|
||||
system("pause");
|
||||
#endif
|
||||
ExitProcess(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
|
||||
{
|
||||
using namespace Globals;
|
||||
const auto exceptionRecord = ep->ExceptionRecord;
|
||||
const auto contextRecord = ep->ContextRecord;
|
||||
|
||||
if (exceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
|
||||
{
|
||||
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
|
||||
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
|
||||
contextRecord->EFlags &= ~0x100; // clear the trap flag
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.AccountDataItem_UpdateNormalProp)) {
|
||||
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::AccountDataItem_UpdateNormalProp);
|
||||
contextRecord->EFlags &= ~0x100; // clear the trap flag
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index)
|
||||
{
|
||||
using namespace Globals;
|
||||
|
||||
if (index > 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
CONTEXT ctx{};
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
GetThreadContext(thread, &ctx);
|
||||
|
||||
DWORD64* dr = &ctx.Dr0;
|
||||
dr[index] = enable ? address : 0;
|
||||
|
||||
const auto mask = 1ull << (index * 2);
|
||||
ctx.Dr7 |= mask;
|
||||
|
||||
SetThreadContext(thread, &ctx);
|
||||
}
|
||||
|
||||
DWORD __stdcall ThreadProc(LPVOID hInstance)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
AllocConsole();
|
||||
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
|
||||
system("pause");
|
||||
#endif
|
||||
InitializeCriticalSection(&CriticalSection);
|
||||
|
||||
auto initFuture = std::async(std::launch::async, InitIL2CPP);
|
||||
|
||||
using namespace Globals;
|
||||
const auto pid = GetCurrentProcessId();
|
||||
|
||||
while ((GameWindow = Util::FindMainWindowByPID(pid)) == nullptr) {
|
||||
SwitchToThread();
|
||||
}
|
||||
|
||||
if (!initFuture.get())
|
||||
ExitProcess(0);
|
||||
|
||||
MessagePipe = CreateFileA(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (!MessagePipe)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
std::println("CreateFile failed: {}", GetLastError());
|
||||
#else
|
||||
Util::Win32ErrorDialog(1001, GetLastError());
|
||||
ExitProcess(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
|
||||
while (true)
|
||||
{
|
||||
THREADENTRY32 te32{};
|
||||
te32.dwSize = sizeof(THREADENTRY32);
|
||||
const auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
||||
for (Thread32First(hSnapshot, &te32); Thread32Next(hSnapshot, &te32);)
|
||||
{
|
||||
if (te32.th32OwnerProcessID != pid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID))
|
||||
{
|
||||
EnterCriticalSection(&CriticalSection);
|
||||
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true, 0);
|
||||
SetBreakpoint(hThread, Offset.AccountDataItem_UpdateNormalProp, true, 1);
|
||||
CloseHandle(hThread);
|
||||
LeaveCriticalSection(&CriticalSection);
|
||||
}
|
||||
}
|
||||
CloseHandle(hSnapshot);
|
||||
Sleep(1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// DLL entry point
|
||||
BOOL __stdcall DllMain(HMODULE hInstance, DWORD fdwReason, LPVOID lpReserved)
|
||||
{
|
||||
// Check injectee
|
||||
WCHAR szFileName[MAX_PATH]{};
|
||||
DWORD length = 0;
|
||||
GetModuleFileNameW(NULL, szFileName, MAX_PATH);
|
||||
if (!(wcsstr(szFileName, L"YuanShen.exe") || wcsstr(szFileName, L"GenshinImpact.exe")))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (fdwReason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
if (hInstance)
|
||||
{
|
||||
LdrAddRefDll(LDR_ADDREF_DLL_PIN, hInstance);
|
||||
}
|
||||
|
||||
if (const auto hThread = CreateThread(nullptr, 0, ThreadProc, hInstance, 0, nullptr)) {
|
||||
CloseHandle(hThread);
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static LRESULT WINAPI YaeGetWindowHookImpl(int code, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
return CallNextHookEx(NULL, code, wParam, lParam);
|
||||
}
|
||||
|
||||
EXTERN_C __declspec(dllexport) HRESULT WINAPI YaeGetWindowHook(_Out_ HOOKPROC* pHookProc)
|
||||
{
|
||||
*pHookProc = YaeGetWindowHookImpl;
|
||||
return S_OK;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <unordered_set>
|
||||
#include "NamedPipe.h"
|
||||
|
||||
#define PROPERTY2(type, name, cn, os) \
|
||||
type name##_cn = cn; \
|
||||
type name##_os = os; \
|
||||
type get_##name() { return Globals::IsCNREL ? name##_cn : name##_os; } \
|
||||
void set_##name(type value) { if (Globals::IsCNREL) name##_cn = value; else name##_os = value; } \
|
||||
__declspec(property(get = get_##name, put = set_##name)) type name;
|
||||
|
||||
namespace Globals
|
||||
{
|
||||
inline HWND GameWindow = nullptr;
|
||||
inline NamedPipe MessagePipe = nullptr;
|
||||
inline bool IsCNREL = true;
|
||||
inline uintptr_t BaseAddress = 0;
|
||||
|
||||
// 5.1.0 - 24082
|
||||
inline uint16_t AchievementId = 0; // use non-zero to override dynamic search
|
||||
inline std::unordered_set<uint16_t> AchievementIdSet;
|
||||
|
||||
// 5.3.0 - 23233
|
||||
inline uint16_t PlayerStoreId = 0; // use non-zero to override dynamic search
|
||||
|
||||
inline bool AchievementsWritten = false;
|
||||
inline bool PlayerStoreWritten = false;
|
||||
|
||||
/*
|
||||
* 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,
|
||||
*/
|
||||
inline std::unordered_set<int> RequiredPlayerProperties = { 10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058 };
|
||||
|
||||
class Offsets
|
||||
{
|
||||
public:
|
||||
PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0, 0);
|
||||
//PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0x0F826CF0, 0x0F825F10); // use non-zero to override dynamic search
|
||||
PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0, 0);
|
||||
//PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0x0D9FE060, 0x0D94D910); // use non-zero to override dynamic search
|
||||
};
|
||||
|
||||
inline Offsets Offset;
|
||||
}
|
||||
@@ -1,588 +0,0 @@
|
||||
#include <Windows.h>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iterator>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <immintrin.h>
|
||||
|
||||
#include "globals.h"
|
||||
#include "Zydis.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
class DecodedInstruction
|
||||
{
|
||||
public:
|
||||
DecodedInstruction() = default;
|
||||
~DecodedInstruction() = default;
|
||||
DecodedInstruction(const ZydisDecodedInstruction& instruction) : Instruction(instruction) {}
|
||||
DecodedInstruction(const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : Instruction(instruction) {
|
||||
Operands = { operands, operands + operandCount };
|
||||
}
|
||||
DecodedInstruction(const uint32_t rva, const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : RVA(rva), Instruction(instruction) {
|
||||
Operands = { operands, operands + operandCount };
|
||||
}
|
||||
|
||||
// copy constructor
|
||||
DecodedInstruction(const DecodedInstruction& other) = default;
|
||||
|
||||
// move constructor
|
||||
DecodedInstruction(DecodedInstruction&& other) noexcept : RVA(other.RVA), Instruction(other.Instruction), Operands(std::move(other.Operands)) {}
|
||||
|
||||
uint32_t RVA = 0;
|
||||
ZydisDecodedInstruction Instruction;
|
||||
std::vector<ZydisDecodedOperand> Operands;
|
||||
};
|
||||
|
||||
std::span<uint8_t> GetSection(LPCSTR name)
|
||||
{
|
||||
using namespace Globals;
|
||||
if (BaseAddress == 0)
|
||||
return {};
|
||||
|
||||
const auto dosHeader = (PIMAGE_DOS_HEADER)BaseAddress;
|
||||
const auto ntHeader = (PIMAGE_NT_HEADERS)((uintptr_t)dosHeader + dosHeader->e_lfanew);
|
||||
const auto sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
|
||||
|
||||
for (auto i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
|
||||
{
|
||||
if (strcmp((char*)sectionHeader[i].Name, name) == 0)
|
||||
{
|
||||
const auto sectionSize = sectionHeader[i].Misc.VirtualSize;
|
||||
const auto virtualAddress = BaseAddress + sectionHeader[i].VirtualAddress;
|
||||
return std::span(reinterpret_cast<uint8_t*>(virtualAddress), sectionSize);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// decodes all instruction until next push, ignores branching
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="maxInstructions"></param>
|
||||
/// <returns>std::vector DecodedInstruction</returns>
|
||||
std::vector<DecodedInstruction> DecodeFunction(uintptr_t address, int32_t maxInstructions = -1)
|
||||
{
|
||||
using namespace Globals;
|
||||
|
||||
std::vector<DecodedInstruction> instructions;
|
||||
|
||||
ZydisDecoder decoder{};
|
||||
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
|
||||
|
||||
ZydisDecodedInstruction instruction{};
|
||||
ZydisDecoderContext context{};
|
||||
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT_VISIBLE]{};
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto data = reinterpret_cast<uint8_t*>(address);
|
||||
auto status = ZydisDecoderDecodeInstruction(&decoder, &context, data, ZYDIS_MAX_INSTRUCTION_LENGTH, &instruction);
|
||||
if (!ZYAN_SUCCESS(status))
|
||||
{
|
||||
// for skipping jump tables
|
||||
address += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
status = ZydisDecoderDecodeOperands(&decoder, &context, &instruction, operands, instruction.operand_count_visible);
|
||||
if (!ZYAN_SUCCESS(status))
|
||||
{
|
||||
// for skipping jump tables
|
||||
address += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction.mnemonic == ZYDIS_MNEMONIC_PUSH && !instructions.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rva = static_cast<uint32_t>(address - BaseAddress);
|
||||
instructions.emplace_back(rva, instruction, operands, instruction.operand_count_visible);
|
||||
|
||||
address += instruction.length;
|
||||
|
||||
if (maxInstructions != -1 && instructions.size() >= maxInstructions)
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the count of data references in the instructions (only second oprand of mov)
|
||||
/// </summary>
|
||||
/// <param name="instructions"></param>
|
||||
/// <returns></returns>
|
||||
int32_t GetDataReferenceCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_MOV)
|
||||
return false;
|
||||
|
||||
if (instr.Operands.size() != 2)
|
||||
return false;
|
||||
|
||||
const auto& op = instr.Operands[1];
|
||||
|
||||
// access to memory, based off of rip, 32-bit displacement
|
||||
return op.type == ZYDIS_OPERAND_TYPE_MEMORY && op.mem.base == ZYDIS_REGISTER_RIP && op.mem.disp.has_displacement;
|
||||
}));
|
||||
}
|
||||
|
||||
int32_t GetCallCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL;
|
||||
}));
|
||||
}
|
||||
|
||||
int32_t GetUniqueCallCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
std::unordered_set<uint32_t> calls;
|
||||
for (const auto& instr : instructions)
|
||||
{
|
||||
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) {
|
||||
uint32_t destination = instr.Operands[0].imm.value.s + instr.RVA + instr.Instruction.length;
|
||||
calls.insert(destination);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int32_t>(calls.size());
|
||||
}
|
||||
|
||||
int32_t GetCmpImmCount(const std::vector<DecodedInstruction>& instructions)
|
||||
{
|
||||
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
|
||||
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP && instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && instr.Operands[1].imm.value.u;
|
||||
}));
|
||||
}
|
||||
|
||||
void ResolveAchivementCmdId()
|
||||
{
|
||||
if (Globals::AchievementId != 0)
|
||||
return;
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
if (il2cppSection.empty())
|
||||
return; // message box?
|
||||
|
||||
const auto candidates = Util::PatternScanAll(il2cppSection, "56 48 83 EC 20 48 89 D0 48 89 CE 80 3D ? ? ? ? 00");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
|
||||
std::vector<std::vector<DecodedInstruction>> filteredInstructions;
|
||||
std::ranges::copy_if(
|
||||
candidates | std::views::transform([](auto va) { return DecodeFunction(va); }),
|
||||
std::back_inserter(filteredInstructions),
|
||||
[](const std::vector<DecodedInstruction>& instr) {
|
||||
return GetDataReferenceCount(instr) == 5 && GetCallCount(instr) == 10 &&
|
||||
GetUniqueCallCount(instr) == 6 && GetCmpImmCount(instr) == 5;
|
||||
});
|
||||
|
||||
// should have only one result
|
||||
if (filteredInstructions.size() != 1)
|
||||
{
|
||||
std::println("Filtered Instructions: {}", filteredInstructions.size());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& instructions = filteredInstructions[0];
|
||||
std::println("RVA: 0x{:08X}", instructions.front().RVA);
|
||||
|
||||
// extract all the non-zero immediate values from the cmp instructions
|
||||
std::vector<uint32_t> cmdIds;
|
||||
std::ranges::for_each(instructions, [&cmdIds](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
|
||||
instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
|
||||
instr.Operands[1].imm.value.u != 0) {
|
||||
cmdIds.push_back(static_cast<uint32_t>(instr.Operands[1].imm.value.u));
|
||||
}
|
||||
});
|
||||
|
||||
for (const auto& cmdId : cmdIds)
|
||||
{
|
||||
std::println("AchievementId: {}", cmdId);
|
||||
Globals::AchievementIdSet.insert(static_cast<uint16_t>(cmdId));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::vector<uintptr_t> GetCalls(uint8_t* target)
|
||||
{
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
const auto sectionAddress = reinterpret_cast<uintptr_t>(il2cppSection.data());
|
||||
const auto sectionSize = il2cppSection.size();
|
||||
|
||||
std::vector<uintptr_t> callSites;
|
||||
const __m128i callOpcode = _mm_set1_epi8(0xE8);
|
||||
const size_t simdEnd = sectionSize / 16 * 16;
|
||||
|
||||
for (size_t i = 0; i < simdEnd; i += 16) {
|
||||
// load 16 bytes from the current address
|
||||
const __m128i chunk = _mm_loadu_si128((__m128i*)(sectionAddress + i));
|
||||
|
||||
// compare the loaded chunk with 0xE8 in all 16 bytes
|
||||
const __m128i result = _mm_cmpeq_epi8(chunk, callOpcode);
|
||||
|
||||
// move the comparison results into a mask
|
||||
int mask = _mm_movemask_epi8(result);
|
||||
|
||||
while (mask != 0) {
|
||||
DWORD first_match_idx = 0;
|
||||
_BitScanForward(&first_match_idx, mask); // index of the first set bit (match)
|
||||
|
||||
// index of the instruction
|
||||
const size_t instruction_index = i + first_match_idx;
|
||||
|
||||
const int32_t delta = *(int32_t*)(sectionAddress + instruction_index + 1);
|
||||
const uintptr_t dest = sectionAddress + instruction_index + 5 + delta;
|
||||
|
||||
if (dest == (uintptr_t)target) {
|
||||
callSites.push_back(sectionAddress + instruction_index);
|
||||
}
|
||||
|
||||
// clear the bit we just processed and continue with the next match
|
||||
mask &= ~(1 << first_match_idx);
|
||||
}
|
||||
}
|
||||
|
||||
return callSites;
|
||||
}
|
||||
|
||||
uintptr_t FindFunctionEntry(uintptr_t address) // not a correct way to find function entry
|
||||
{
|
||||
__try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// go back to 'sub rsp' instruction
|
||||
uint32_t code = *(uint32_t*)address;
|
||||
code &= ~0xFF000000;
|
||||
|
||||
if (_byteswap_ulong(code) == 0x4883EC00) { // sub rsp, ??
|
||||
return address;
|
||||
}
|
||||
|
||||
address--;
|
||||
}
|
||||
|
||||
}
|
||||
__except (1) {}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
void Resolve_BitConverter_ToUInt16()
|
||||
{
|
||||
if (Globals::Offset.BitConverter_ToUInt16 != 0) {
|
||||
Globals::Offset.BitConverter_ToUInt16 += Globals::BaseAddress;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
std::print("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
/*
|
||||
mov ecx, 0Fh
|
||||
call ThrowHelper.ThrowArgumentNullException
|
||||
mov ecx, 0Eh
|
||||
mov edx, 16h
|
||||
call ThrowHelper.ThrowArgumentOutOfRangeException
|
||||
mov ecx, 5
|
||||
call ThrowHelper.ThrowArgumentException
|
||||
*/
|
||||
auto candidates = Util::PatternScanAll(il2cppSection, "B9 0F 00 00 00 E8 ? ? ? ? B9 0E 00 00 00 BA 16 00 00 00 E8 ? ? ? ? B9 05 00 00 00 E8 ? ? ? ?");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
|
||||
std::vector<uintptr_t> filteredEntries;
|
||||
std::ranges::copy_if(candidates, std::back_inserter(filteredEntries), [](uintptr_t& entry) {
|
||||
entry = FindFunctionEntry(entry);
|
||||
return entry % 16 == 0;
|
||||
});
|
||||
|
||||
for (const auto& entry : filteredEntries)
|
||||
{
|
||||
std::println("Entry: 0x{:X}", entry);
|
||||
}
|
||||
|
||||
std::println("Looking for call counts...");
|
||||
std::mutex mutex;
|
||||
std::unordered_map<uintptr_t, int32_t> callCounts;
|
||||
// find the call counts to candidate functions
|
||||
std::vector<std::future<void>> futures;
|
||||
std::ranges::transform(filteredEntries, std::back_inserter(futures), [&](uintptr_t entry) {
|
||||
return std::async(std::launch::async, [&](uintptr_t e) {
|
||||
const auto callSites = GetCalls((uint8_t*)e);
|
||||
std::lock_guard lock(mutex);
|
||||
callCounts[e] = callSites.size();
|
||||
}, entry);
|
||||
});
|
||||
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
}
|
||||
|
||||
uintptr_t targetEntry = 0;
|
||||
for (const auto& [entry, count] : callCounts)
|
||||
{
|
||||
std::println("Entry: 0x{:X}, RVA: 0x{:08X}, Count: {}", entry, entry - Globals::BaseAddress, count);
|
||||
if (count == 3) {
|
||||
targetEntry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
Globals::Offset.BitConverter_ToUInt16 = targetEntry;
|
||||
}
|
||||
|
||||
void ResolveInventoryCmdId()
|
||||
{
|
||||
if (Globals::PlayerStoreId != 0)
|
||||
return;
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
std::println("Section Address: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data()));
|
||||
std::println("Section End: 0x{:X}", reinterpret_cast<uintptr_t>(il2cppSection.data() + il2cppSection.size()));
|
||||
|
||||
/*
|
||||
cmp r8d, 2
|
||||
jz 0x3B
|
||||
cmd r8d, 1
|
||||
mov rax
|
||||
*/
|
||||
|
||||
// look for ItemModule.GetBagManagerByStoreType <- mf got inlined in 5.5
|
||||
// we just gon to look for OnPlayerStoreNotify
|
||||
const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 41 83 F8 01");
|
||||
std::println("Candidates: {}", candidates.size());
|
||||
if (candidates.empty())
|
||||
return;
|
||||
|
||||
uintptr_t pOnPlayerStoreNotify = 0;
|
||||
{
|
||||
// one of the candidates is OnPlayerStoreNotify
|
||||
// search after the pattern to find an arbirary branch
|
||||
auto decodedInstructions = candidates | std::views::transform([](auto va) { return DecodeFunction(va, 20); });
|
||||
|
||||
// find the call site with an arbitrary branch (JMP or CALL) after the call
|
||||
auto targetInstructions = std::ranges::find_if(decodedInstructions, [](const auto& instr) {
|
||||
return std::ranges::any_of(instr, [](const DecodedInstruction& i) {
|
||||
return (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JMP || i.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) &&
|
||||
i.Operands.size() == 1 && i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER;
|
||||
});
|
||||
});
|
||||
|
||||
if (targetInstructions == decodedInstructions.end()) {
|
||||
std::println("Failed to find target instruction");
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemModule.OnPlayerStoreNotify
|
||||
const auto& instructions = *targetInstructions;
|
||||
pOnPlayerStoreNotify = Globals::BaseAddress + instructions.front().RVA;
|
||||
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
return (va % 16 == 0 &&
|
||||
code[0] == 0x56 && // push rsi
|
||||
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 126);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPlayerStoreNotify - i); });
|
||||
it != range.end())
|
||||
{
|
||||
pOnPlayerStoreNotify -= *it;
|
||||
}
|
||||
else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
std::println("OnPlayerStoreNotify: 0x{:X}", pOnPlayerStoreNotify);
|
||||
}
|
||||
|
||||
uintptr_t pOnPacket = 0;
|
||||
{
|
||||
// get all calls to OnPlayerStoreNotify
|
||||
const auto calls = GetCalls(reinterpret_cast<uint8_t*>(pOnPlayerStoreNotify));
|
||||
if (calls.size() != 1) {
|
||||
std::println("Failed to find call site");
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemModule.OnPacket - search backwards for function entry
|
||||
pOnPacket = calls.front();
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
return (va % 16 == 0 &&
|
||||
code[0] == 0x56 && // push rsi
|
||||
(*reinterpret_cast<uint32_t*>(&code[1]) & ~0xFF000000) == _byteswap_ulong(0x4883EC00)); // sub rsp, ??
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 3044);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(pOnPacket - i); });
|
||||
it != range.end())
|
||||
{
|
||||
pOnPacket -= *it;
|
||||
}
|
||||
else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
std::println("OnPacket: 0x{:X}", pOnPacket);
|
||||
}
|
||||
|
||||
const auto decodedInstructions = DecodeFunction(pOnPacket);
|
||||
uint32_t cmdid = 0;
|
||||
std::ranges::for_each(decodedInstructions, [&cmdid, pOnPlayerStoreNotify](const DecodedInstruction& i) {
|
||||
static uint32_t immValue = 0; // keep track of the last immediate value
|
||||
|
||||
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
|
||||
i.Operands.size() == 2 &&
|
||||
i.Operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
||||
i.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE)
|
||||
{
|
||||
immValue = static_cast<uint32_t>(i.Operands[1].imm.value.u);
|
||||
}
|
||||
|
||||
if (i.Instruction.meta.branch_type == ZYDIS_BRANCH_TYPE_NEAR && i.Operands.size() == 1 &&
|
||||
(i.Instruction.mnemonic == ZYDIS_MNEMONIC_JZ || i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ)) // jz for true branch, jnz for false branch
|
||||
{
|
||||
// assume the branching is jz
|
||||
uintptr_t branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length + i.Operands[0].imm.value.s;
|
||||
|
||||
// check if the branch is jnz and adjust the branch address
|
||||
if (i.Instruction.mnemonic == ZYDIS_MNEMONIC_JNZ) {
|
||||
branchAddr = Globals::BaseAddress + i.RVA + i.Instruction.length;
|
||||
}
|
||||
|
||||
// decode the branch address immediately
|
||||
const auto instructions = DecodeFunction(branchAddr, 10);
|
||||
const auto isMatch = std::ranges::any_of(instructions, [pOnPlayerStoreNotify](const DecodedInstruction& instr) {
|
||||
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_CALL)
|
||||
return false;
|
||||
|
||||
uintptr_t destination = 0;
|
||||
ZydisCalcAbsoluteAddress(&instr.Instruction, instr.Operands.data(), Globals::BaseAddress + instr.RVA, &destination);
|
||||
return destination == pOnPlayerStoreNotify;
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
cmdid = immValue;
|
||||
}
|
||||
|
||||
}
|
||||
return cmdid == 0; // stop processing if cmdid is found
|
||||
});
|
||||
|
||||
Globals::PlayerStoreId = static_cast<uint16_t>(cmdid);
|
||||
std::println("PlayerStoreId: {}", Globals::PlayerStoreId);
|
||||
}
|
||||
|
||||
void Resolve_AccountDataItem_UpdateNormalProp()
|
||||
{
|
||||
if (Globals::Offset.AccountDataItem_UpdateNormalProp != 0) {
|
||||
Globals::Offset.AccountDataItem_UpdateNormalProp += Globals::BaseAddress;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto il2cppSection = GetSection("il2cpp");
|
||||
|
||||
/*
|
||||
add ??, 0FFFFD8EEh
|
||||
cmp ??, 30h
|
||||
*/
|
||||
auto candidates = Util::PatternScanAll(il2cppSection, "81 ? EE D8 FF FF ? 83 ? 30");
|
||||
// should have only one result
|
||||
if (candidates.size() != 1)
|
||||
{
|
||||
std::println("Filtered Instructions: {}", candidates.size());
|
||||
return;
|
||||
}
|
||||
auto fp = candidates[0];
|
||||
|
||||
const auto isFunctionEntry = [](uintptr_t va) -> bool {
|
||||
auto* code = reinterpret_cast<uint8_t*>(va);
|
||||
/* push rsi */
|
||||
/* push rdi */
|
||||
return (va % 16 == 0 && code[0] == 0x56 && code[1] == 0x57);
|
||||
};
|
||||
|
||||
auto range = std::views::iota(0, 213);
|
||||
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(fp - i); }); it != range.end()) {
|
||||
fp -= *it;
|
||||
} else {
|
||||
std::println("Failed to find function entry");
|
||||
return;
|
||||
}
|
||||
|
||||
Globals::Offset.AccountDataItem_UpdateNormalProp = fp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool InitIL2CPP()
|
||||
{
|
||||
std::string buffer;
|
||||
buffer.resize(MAX_PATH);
|
||||
ZeroMemory(buffer.data(), MAX_PATH);
|
||||
const auto pathLength = GetModuleFileNameA(nullptr, buffer.data(), MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
buffer.resize(pathLength);
|
||||
ZeroMemory(buffer.data(), pathLength);
|
||||
GetModuleFileNameA(nullptr, buffer.data(), pathLength);
|
||||
}
|
||||
buffer.shrink_to_fit();
|
||||
|
||||
using namespace Globals;
|
||||
IsCNREL = buffer.find("YuanShen.exe") != std::string::npos;
|
||||
BaseAddress = (uintptr_t)GetModuleHandleA(nullptr);
|
||||
|
||||
std::future<void> resolveFuncFuture = std::async(std::launch::async, Resolve_BitConverter_ToUInt16);
|
||||
std::future<void> resolveCmdIdFuture = std::async(std::launch::async, ResolveAchivementCmdId);
|
||||
std::future<void> resolveInventoryFuture = std::async(std::launch::async, ResolveInventoryCmdId);
|
||||
std::future<void> resolveUpdatePropFuture = std::async(std::launch::async, Resolve_AccountDataItem_UpdateNormalProp);
|
||||
|
||||
resolveFuncFuture.get();
|
||||
resolveCmdIdFuture.get();
|
||||
resolveInventoryFuture.get();
|
||||
resolveUpdatePropFuture.get();
|
||||
|
||||
std::println("BaseAddress: 0x{:X}", BaseAddress);
|
||||
std::println("IsCNREL: {:d}", IsCNREL);
|
||||
std::println("BitConverter_ToUInt16: 0x{:X}", Offset.BitConverter_ToUInt16);
|
||||
std::println("AccountDataItem_UpdateNormalProp: 0x{:X}", Offset.AccountDataItem_UpdateNormalProp);
|
||||
|
||||
if (!AchievementId && AchievementIdSet.empty())
|
||||
{
|
||||
Util::ErrorDialog("Failed to resolve achievement data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PlayerStoreId)
|
||||
{
|
||||
Util::ErrorDialog("Failed to resolve inventory data");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// IL2CPP application initializer
|
||||
bool InitIL2CPP();
|
||||
@@ -1,78 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
#define PROPERTY_GET_CONST(type, name, funcBody) \
|
||||
type get_##name() const funcBody \
|
||||
__declspec(property(get = get_##name)) type name;
|
||||
|
||||
enum class PacketType : uint8_t
|
||||
{
|
||||
None = 0,
|
||||
Achivement = 1,
|
||||
Inventory = 2,
|
||||
PropData = 100,
|
||||
End = 255,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Array
|
||||
{
|
||||
public:
|
||||
void* klass;
|
||||
void* monitor;
|
||||
void* bounds;
|
||||
size_t max_length;
|
||||
T vector[1];
|
||||
|
||||
Array() = delete;
|
||||
|
||||
T* data() {
|
||||
return vector;
|
||||
}
|
||||
|
||||
std::span<T> AsSpan() {
|
||||
return { vector, max_length };
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
U As() {
|
||||
return reinterpret_cast<U>(vector);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(alignof(Array<uint8_t>) == 8, "Array alignment is incorrect");
|
||||
static_assert(offsetof(Array<uint8_t>, vector) == 32, "vector offset is incorrect");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
class PacketMeta
|
||||
{
|
||||
uint16_t m_HeadMagic;
|
||||
uint16_t m_CmdId;
|
||||
uint16_t m_HeaderLength;
|
||||
uint32_t m_DataLength;
|
||||
uint8_t m_Data[1];
|
||||
public:
|
||||
|
||||
PacketMeta() = delete;
|
||||
|
||||
PROPERTY_GET_CONST(uint16_t, HeadMagic, { return _byteswap_ushort(m_HeadMagic); });
|
||||
PROPERTY_GET_CONST(uint16_t, CmdId, { return _byteswap_ushort(m_CmdId); });
|
||||
PROPERTY_GET_CONST(uint16_t, HeaderLength, { return _byteswap_ushort(m_HeaderLength); });
|
||||
PROPERTY_GET_CONST(uint32_t, DataLength, { return _byteswap_ulong(m_DataLength); });
|
||||
|
||||
std::span<uint8_t> AsSpan() {
|
||||
return { m_Data + HeaderLength, DataLength };
|
||||
}
|
||||
|
||||
friend struct PacketMetaStaticAssertHelper;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct PacketMetaStaticAssertHelper
|
||||
{
|
||||
static_assert(offsetof(PacketMeta, m_CmdId) == 2, "CmdId offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_HeaderLength) == 4, "HeadLength offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_DataLength) == 6, "DataLength offset is incorrect");
|
||||
static_assert(offsetof(PacketMeta, m_Data) == 10, "Data offset is incorrect");
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
#pragma comment(lib, "ntdll.lib")
|
||||
|
||||
#define LDR_ADDREF_DLL_PIN 0x00000001
|
||||
|
||||
EXTERN_C NTSYSAPI NTSTATUS NTAPI LdrAddRefDll(_In_ ULONG Flags, _In_ PVOID DllHandle);
|
||||
210
lib/src/util.cpp
210
lib/src/util.cpp
@@ -1,210 +0,0 @@
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <ranges>
|
||||
#include <intrin.h>
|
||||
#include "util.h"
|
||||
|
||||
#include "globals.h"
|
||||
|
||||
#ifdef _DEBUG
|
||||
#pragma runtime_checks("", off)
|
||||
#endif
|
||||
|
||||
#pragma region FindMainWindowByPID
|
||||
|
||||
namespace
|
||||
{
|
||||
struct HandleData {
|
||||
DWORD Pid;
|
||||
HWND Hwnd;
|
||||
};
|
||||
|
||||
bool IsMainWindow(HWND handle) {
|
||||
return GetWindow(handle, GW_OWNER) == nullptr && IsWindowVisible(handle) == TRUE;
|
||||
}
|
||||
|
||||
bool IsUnityWindow(HWND handle) {
|
||||
char szName[256]{};
|
||||
GetClassNameA(handle, szName, 256);
|
||||
return _stricmp(szName, "UnityWndClass") == 0;
|
||||
}
|
||||
|
||||
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
|
||||
HandleData& data = *(HandleData*)lParam;
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(handle, &pid);
|
||||
if (data.Pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
|
||||
return TRUE;
|
||||
data.Hwnd = handle;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
std::tuple<std::vector<uint8_t>, std::string> PatternToBytes(const char* pattern)
|
||||
{
|
||||
std::vector<uint8_t> bytes;
|
||||
std::string mask;
|
||||
|
||||
const auto start = const_cast<char*>(pattern);
|
||||
const auto end = const_cast<char*>(pattern) + strlen(pattern);
|
||||
|
||||
for (auto current = start; current < end; ++current) {
|
||||
if (*current == '?') {
|
||||
++current;
|
||||
if (*current == '?')
|
||||
++current;
|
||||
bytes.push_back(-1);
|
||||
mask.push_back('?');
|
||||
}
|
||||
else {
|
||||
bytes.push_back(strtoul(current, ¤t, 16));
|
||||
mask.push_back('x');
|
||||
}
|
||||
}
|
||||
return { bytes, mask };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
static constexpr LPCSTR base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
namespace Util
|
||||
{
|
||||
HWND FindMainWindowByPID(DWORD pid)
|
||||
{
|
||||
HandleData data = {
|
||||
.Pid = pid,
|
||||
.Hwnd = nullptr
|
||||
};
|
||||
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
|
||||
return data.Hwnd;
|
||||
}
|
||||
|
||||
std::string Base64Encode(std::span<uint8_t> data)
|
||||
{
|
||||
return Base64Encode(data.data(), data.size());
|
||||
}
|
||||
|
||||
std::string Base64Encode(uint8_t const* buf, size_t bufLen)
|
||||
{
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
uint8_t char_array_3[3];
|
||||
uint8_t char_array_4[4];
|
||||
while (bufLen--) {
|
||||
char_array_3[i++] = *buf++;
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (i = 0; (i < 4); i++)
|
||||
ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
int j;
|
||||
for (j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (j = 0; j < i + 1; j++)
|
||||
ret += base64_chars[char_array_4[j]];
|
||||
while (i++ < 3)
|
||||
ret += '=';
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ErrorDialog(LPCSTR title, LPCSTR msg)
|
||||
{
|
||||
MessageBoxA(Globals::GameWindow, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL);
|
||||
}
|
||||
|
||||
void ErrorDialog(LPCSTR msg)
|
||||
{
|
||||
ErrorDialog("YaeAchievement", msg);
|
||||
}
|
||||
|
||||
void Win32ErrorDialog(DWORD code, DWORD winerrcode)
|
||||
{
|
||||
const std::string msg = "CRITICAL ERROR!\nError code: " + std::to_string(winerrcode) + "-" + std::to_string(code) +
|
||||
"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!";
|
||||
|
||||
ErrorDialog("YaeAchievement", msg.c_str());
|
||||
}
|
||||
|
||||
std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern)
|
||||
{
|
||||
std::vector<uintptr_t> results;
|
||||
const auto [patternBytes, patternMask] = PatternToBytes(pattern);
|
||||
constexpr std::size_t chunkSize = 16;
|
||||
|
||||
const auto maskCount = static_cast<std::size_t>(std::ceil(patternMask.size() / chunkSize));
|
||||
std::array<int32_t, 32> masks{};
|
||||
|
||||
auto chunks = patternMask | std::views::chunk(chunkSize);
|
||||
for (std::size_t i = 0; auto chunk : chunks) {
|
||||
int32_t mask = 0;
|
||||
for (std::size_t j = 0; j < chunk.size(); ++j) {
|
||||
if (chunk[j] == 'x') {
|
||||
mask |= 1 << j;
|
||||
}
|
||||
}
|
||||
masks[i++] = mask;
|
||||
}
|
||||
|
||||
__m128i xmm1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data()));
|
||||
__m128i xmm2, xmm3, mask;
|
||||
|
||||
auto pData = bytes.data();
|
||||
const auto end = pData + bytes.size() - patternMask.size();
|
||||
|
||||
while (pData < end)
|
||||
{
|
||||
_mm_prefetch(reinterpret_cast<const char*>(pData + 64), _MM_HINT_NTA);
|
||||
|
||||
if (patternBytes[0] == pData[0])
|
||||
{
|
||||
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData));
|
||||
mask = _mm_cmpeq_epi8(xmm1, xmm2);
|
||||
|
||||
if ((_mm_movemask_epi8(mask) & masks[0]) == masks[0])
|
||||
{
|
||||
bool found = true;
|
||||
for (int i = 1; i < maskCount; ++i)
|
||||
{
|
||||
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData + i * chunkSize));
|
||||
xmm3 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(patternBytes.data() + i * chunkSize));
|
||||
mask = _mm_cmpeq_epi8(xmm2, xmm3);
|
||||
if ((_mm_movemask_epi8(mask) & masks[i]) != masks[i])
|
||||
{
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (found) {
|
||||
results.push_back(reinterpret_cast<uintptr_t>(pData));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
++pData;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
#pragma runtime_checks("", restore)
|
||||
#endif
|
||||
@@ -1,19 +0,0 @@
|
||||
// ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace Util
|
||||
{
|
||||
HWND FindMainWindowByPID(DWORD pid);
|
||||
std::string Base64Encode(std::span<uint8_t> data);
|
||||
std::string Base64Encode(uint8_t const* buf, size_t bufLen);
|
||||
|
||||
void ErrorDialog(LPCSTR title, LPCSTR msg);
|
||||
void ErrorDialog(LPCSTR msg);
|
||||
void Win32ErrorDialog(DWORD code, DWORD winerrcode);
|
||||
|
||||
std::vector<uintptr_t> PatternScanAll(std::span<uint8_t> bytes, const char* pattern);
|
||||
}
|
||||
Reference in New Issue
Block a user