13 Commits
5.4.0 ... 5.6.0

Author SHA1 Message Date
HolographicHat
4a1da61904 bump version 2025-06-18 14:44:37 +08:00
HolographicHat
9eb8955fda optimize GetGamePath 2025-06-11 01:47:34 +08:00
HolographicHat
62c08f54ab fix 2025-06-08 00:44:32 +08:00
HolographicHat
645fe38c65 fix #138 2025-06-07 20:35:24 +08:00
HolographicHat
8f9a26a237 fix #2471 2025-06-05 21:56:03 +08:00
HolographicHat
8648b3a308 bump version 2025-06-04 03:16:09 +08:00
HolographicHat
829553b3a6 sentry 2025-06-04 03:14:07 +08:00
HolographicHat
87898eedfa fix 2025-06-04 02:01:00 +08:00
HolographicHat
a10b491886 prefer using field id from config 2025-05-30 01:23:39 +08:00
HolographicHat
e3e7107b14 Update YaeAchievementLib.nuspec 2025-05-08 10:11:05 +08:00
HolographicHat
3231746aa5 Merge pull request #135 from 34736384/master 2025-05-07 15:44:48 +08:00
34736384
5c9cdd46d2 update psn pattern 2025-05-07 13:34:52 +08:00
HolographicHat
881a4bc725 [no ci] update docs 2025-04-13 03:49:58 +08:00
29 changed files with 469 additions and 231 deletions

View File

@@ -13,8 +13,6 @@
## 导出支持 ## 导出支持
> 按照数字键选择导出方式,<kbd>0</kbd> 为默认导出方式
0. [椰羊](https://cocogoat.work/achievement) 0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao) 1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
@@ -35,8 +33,5 @@
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 598720036](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/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## 常见问题 ## 常见问题
0. Q: 打不开
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer)
1. Q: 原神启动时报错: 数据异常(31-4302) 1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起 A: 不要把软件和原神主程序放一起

View File

@@ -14,8 +14,6 @@
## Export support ## 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) 0. [Cocogoat](https://cocogoat.work/achievement)
1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao) 1. [Snap HuTao](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
@@ -35,10 +33,6 @@
[issues](https://github.com/HolographicHat/YaeAchievement/issues) or [QQ群: 598720036](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/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## Frequently asked questions ## Frequently asked questions
0. Q: Unable to start
A: Download and install [.NET Runtime 9](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer) or ` winget install Microsoft.DotNet.Runtime.9`
1. Q: Error while Genshin started: Data Exception (31-4302) 1. Q: Error while Genshin started: Data Exception (31-4302)
A: Don't place software in the directory containing Genshin Impact. A: Don't place software in the directory containing Genshin Impact.

View File

@@ -14,8 +14,6 @@
## エクスポートサポート ## エクスポートサポート
> 数字キーに従ってエクスポート方法を選択します。<kbd>0</kbd> はデフォルトのエクスポート方法です
0. [椰羊](https://cocogoat.work/achievement) 0. [椰羊](https://cocogoat.work/achievement)
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao) 1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
2. [Paimon.moe](https://paimon.moe/achievement/) 2. [Paimon.moe](https://paimon.moe/achievement/)
@@ -35,10 +33,6 @@
[issues](https://github.com/HolographicHat/YaeAchievement/issues) または [QQ群: 598720036](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/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## よくある質問 ## よくある質問
0. Q: 起動できない
A: [.NET Runtime 9](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer) をダウンロードしてインストールするか、`winget install Microsoft.DotNet.Runtime.9` を実行してください。
1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302) 1. Q: 原神を起動中にエラーが発生しました: データ例外 (31-4302)
A: ソフトウェアを原神のディレクトリに配置しないでください。 A: ソフトウェアを原神のディレクトリに配置しないでください。

View File

@@ -8,19 +8,7 @@
![1](https://github.com/user-attachments/assets/8b98c018-b179-4681-992d-367a0f522dae) ![1](https://github.com/user-attachments/assets/8b98c018-b179-4681-992d-367a0f522dae)
2.安装启动软件所需文件(若已安装该运行时可忽略此步骤) 2.打开主程序所需的操作以及成就导出的选择
点击该网址https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer 。
进入网页后浏览器会自动弹出下载,同样地,将文件保存在桌面或者其它易于寻找的文件夹内。
下载完成后打开名称形如dotnet-runtime-x.x.x-win-x64.exe的文件会弹出安装窗口如下图所示。
![2](https://github.com/user-attachments/assets/4830a824-34c0-479e-9c9c-fc23e99003bf)
直接点击安装即可。
3.打开主程序所需的操作以及成就导出的选择
双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。 双击在第一步下载的名称为“YaeAchievement.exe”的文件成功打开后会提示原神正在启动如下图所示。

View File

@@ -9,20 +9,6 @@ you save this file in a desktop or other easy-to-see folder.
![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3) ![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
2.Install .NET Runtime 9 (this step can be ignored if the runtime is already installed)
Click Herehttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer .
Or `winget install Microsoft.DotNet.Runtime.9` 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.
![Guide2](https://github.com/user-attachments/assets/35f421af-dd45-41ea-94f9-e3cf90710f0f)
Just click Install.
3.The actions required to open the main program and the options for the achievement export 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. 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.

View File

@@ -8,20 +8,6 @@
![Guide1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3) ![Guide1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)
2. .NET Runtime 9をインストールランタイムが既にインストールされている場合はこのステップを無視できます
こちらをクリックhttps://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-9.0.3-windows-x64-installer .
または、Windows 11を使用しているか、Wingetがインストールされている場合は、`winget install Microsoft.DotNet.Runtime.9`を実行します。
ウェブページにアクセスすると、ブラウザが自動的にポップアップしてダウンロードされます。ファイルをデスクトップや他の見やすいフォルダに保存します。
ダウンロードが完了したら、dotnet-runtime-x.x.x-win-x64.exeという名前のファイルを開くと、インストールウィンドウがポップアップします。以下の図のように表示されます。
![Guide2](https://github.com/user-attachments/assets/35f421af-dd45-41ea-94f9-e3cf90710f0f)
インストールをクリックするだけです。
3. メインプログラムを開くための操作と実績エクスポートのオプション 3. メインプログラムを開くための操作と実績エクスポートのオプション
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。 最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。

View File

@@ -18,4 +18,9 @@ TerminateProcess
VirtualAllocEx VirtualAllocEx
VirtualFreeEx VirtualFreeEx
WaitForSingleObject WaitForSingleObject
WriteProcessMemory WriteProcessMemory
GetCurrentConsoleFontEx
OpenProcess
GetModuleFileNameEx

View File

@@ -16,8 +16,7 @@
<PropertyGroup> <PropertyGroup>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IlcOptimizationPreference>Size</IlcOptimizationPreference> <OptimizationPreference>Size</OptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -29,6 +28,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Sentry" Version="5.6.0" />
<PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.3" /> <PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.3" />
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0"> <PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@@ -105,7 +105,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You need to login genshin impact before exporting.. /// Looks up a localized string similar to Please launch GenshinImpact to continue..
/// </summary> /// </summary>
internal static string ConfigNeedStartGenshin { internal static string ConfigNeedStartGenshin {
get { get {
@@ -374,6 +374,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> /// <summary>
/// Looks up a localized string similar to Reward not taken. /// Looks up a localized string similar to Reward not taken.
/// </summary> /// </summary>
@@ -410,6 +446,15 @@ 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> /// <summary>
/// Looks up a localized string similar to Checking update.... /// Looks up a localized string similar to Checking update....
/// </summary> /// </summary>
@@ -438,6 +483,15 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to The process cannot access the file &apos;{0}&apos; 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> /// <summary>
/// Looks up a localized string similar to Has update: {0} =&gt; {1}. /// Looks up a localized string similar to Has update: {0} =&gt; {1}.
/// </summary> /// </summary>

View File

@@ -61,7 +61,7 @@
<value>Reward not taken</value> <value>Reward not taken</value>
</data> </data>
<data name="ConfigNeedStartGenshin" xml:space="preserve"> <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>
<data name="DownloadLink" xml:space="preserve"> <data name="DownloadLink" xml:space="preserve">
<value>Download: {0}</value> <value>Download: {0}</value>
@@ -163,4 +163,22 @@
<data name="ExportTargetWxApp1" xml:space="preserve"> <data name="ExportTargetWxApp1" xml:space="preserve">
<value /> <value />
</data> </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>
</root> </root>

View File

@@ -18,7 +18,7 @@
<value>全部成就</value> <value>全部成就</value>
</data> </data>
<data name="ExportChoose" xml:space="preserve"> <data name="ExportChoose" xml:space="preserve">
<value>要导出到哪里?(键盘上下键移动光标,键盘回车键选择)</value> <value>要导出到哪里?</value>
</data> </data>
<data name="ExportToCocogoatSuccess" xml:space="preserve"> <data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>在浏览器内进行下一步操作</value> <value>在浏览器内进行下一步操作</value>
@@ -54,7 +54,7 @@
<value>已完成</value> <value>已完成</value>
</data> </data>
<data name="ConfigNeedStartGenshin" xml:space="preserve"> <data name="ConfigNeedStartGenshin" xml:space="preserve">
<value>在导出前你需要先完成一次登入流程.</value> <value>请打开 原神 后继续操作</value>
</data> </data>
<data name="DownloadLink" xml:space="preserve"> <data name="DownloadLink" xml:space="preserve">
<value>下载地址: {0}</value> <value>下载地址: {0}</value>
@@ -91,7 +91,7 @@
<value>YaeAchievement - 原神成就导出工具 ({0})</value> <value>YaeAchievement - 原神成就导出工具 ({0})</value>
</data> </data>
<data name="UsePreviousData" xml:space="preserve"> <data name="UsePreviousData" xml:space="preserve">
<value>要使用上一次获取到的成就数据吗?(键盘上下键移动光标,键盘回车键选择)</value> <value>要使用上一次获取到的成就数据吗?</value>
</data> </data>
<data name="NetworkError" xml:space="preserve"> <data name="NetworkError" xml:space="preserve">
<value>网络错误: {0}</value> <value>网络错误: {0}</value>
@@ -156,4 +156,22 @@
<data name="ExportTargetWxApp1" xml:space="preserve"> <data name="ExportTargetWxApp1" xml:space="preserve">
<value>原魔工具箱</value> <value>原魔工具箱</value>
</data> </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>
</root> </root>

View File

@@ -1,5 +1,8 @@
using System.Text.RegularExpressions; using System.Diagnostics.CodeAnalysis;
using YaeAchievement.res; using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Spectre.Console;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
namespace YaeAchievement; namespace YaeAchievement;
@@ -13,26 +16,66 @@ public static partial class AppConfig {
internal static void Load(string argumentPath) { internal static void Load(string argumentPath) {
if (argumentPath != "auto" && File.Exists(argumentPath)) { if (argumentPath != "auto" && File.Exists(argumentPath)) {
GamePath = argumentPath; GamePath = argumentPath;
return; } else if (TryReadGamePathFromCache(out var cachedPath)) {
GamePath = cachedPath;
} else if (TryReadGamePathFromUnityLog(out var loggedPath)) {
GamePath = loggedPath;
} else {
GamePath = ReadGamePathFromProcess();
} }
if (CacheFile.TryRead("genshin_impact_game_path", out var cache)) { Span<byte> buffer = stackalloc byte[0x10000];
var path = cache.Content.ToStringUtf8(); using var stream = File.OpenRead(GamePath);
if (path != null && File.Exists(path)) { if (stream.Read(buffer) == buffer.Length) {
GamePath = path; var hash = Convert.ToHexString(MD5.HashData(buffer));
return; CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{hash}"));
}
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");
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(cacheData[0]);
if (stream.Read(buffer) != buffer.Length || Convert.ToHexString(MD5.HashData(buffer)) != cacheData[1]) {
return false;
}
path = cacheData[0];
return true;
} catch (Exception) {
return false;
} }
} }
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); static bool TryReadGamePathFromUnityLog([NotNullWhen(true)] out string? path) {
var logPath = ProductNames path = null;
.Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt") try {
.Where(File.Exists) var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
.MaxBy(File.GetLastWriteTime); var logPath = ProductNames
if (logPath == null) { .Select(name => $"{appDataPath}/../LocalLow/miHoYo/{name}/output_log.txt")
throw new ApplicationException(App.ConfigNeedStartGenshin); .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) { private static string? GetGamePathFromLogFile(string path) {

View File

@@ -1,5 +1,4 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -8,7 +7,7 @@ using Microsoft.Win32;
using Spectre.Console; using Spectre.Console;
using YaeAchievement.Outputs; using YaeAchievement.Outputs;
using YaeAchievement.Parsers; using YaeAchievement.Parsers;
using YaeAchievement.res; using YaeAchievement.Utilities;
// ReSharper disable UnusedMember.Local // ReSharper disable UnusedMember.Local
@@ -33,8 +32,8 @@ public static class Export {
}; };
Action<AchievementAllDataNotify> action; Action<AchievementAllDataNotify> action;
if (ExportTo == 114514) { if (ExportTo == 114514) {
var prompt = new SelectionPrompt<string>().Title(App.ExportChoose).AddChoices(targets.Keys); var prompt = new SelectionPromptCompat<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[AnsiConsole.Prompt(prompt)]; action = targets[prompt.Prompt()];
} else { } else {
action = targets.ElementAtOrDefault(ExportTo).Value ?? ToCocogoat; action = targets.ElementAtOrDefault(ExportTo).Value ?? ToCocogoat;
} }
@@ -210,7 +209,7 @@ public static class Export {
} }
} }
public class WxApp1Root { public sealed class WxApp1Root {
public string Key { get; init; } = null!; public string Key { get; init; } = null!;
@@ -223,7 +222,7 @@ public class WxApp1Root {
GenerationMode = JsonSourceGenerationMode.Serialization, GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower 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 { public static string Serialize(AchievementAllDataNotify ntf, string key) => JsonSerializer.Serialize(new WxApp1Root {
Key = key, Key = key,
@@ -231,8 +230,8 @@ public partial class WxApp1Serializer : JsonSerializerContext {
}, Default.WxApp1Root); }, Default.WxApp1Root);
} }
public record CocogoatResponse(string Key); public sealed record CocogoatResponse(string Key);
[JsonSerializable(typeof(CocogoatResponse))] [JsonSerializable(typeof(CocogoatResponse))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class CocogoatResponseContext : JsonSerializerContext; public sealed partial class CocogoatResponseContext : JsonSerializerContext;

View File

@@ -1,4 +1,7 @@
using System.Diagnostics.CodeAnalysis; global using System.Diagnostics;
global using YaeAchievement.res;
using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using Proto; using Proto;
@@ -15,8 +18,8 @@ public static class GlobalVars {
public static readonly string CachePath = Path.Combine(DataPath, "cache"); public static readonly string CachePath = Path.Combine(DataPath, "cache");
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll"); public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
public const uint AppVersionCode = 236; public const uint AppVersionCode = 238;
public const string AppVersionName = "5.4"; public const string AppVersionName = "5.6";
public const string PipeName = "YaeAchievementPipe"; public const string PipeName = "YaeAchievementPipe";

View File

@@ -8,7 +8,7 @@ namespace YaeAchievement.Outputs;
// ReSharper disable PropertyCanBeMadeInitOnly.Global // ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.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!; public Dictionary<uint, Dictionary<uint, bool>> Achievement { get; set; } = null!;
@@ -29,7 +29,7 @@ public class PaimonRoot {
GenerationMode = JsonSourceGenerationMode.Serialization, GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)] )]
public partial class PaimonSerializer : JsonSerializerContext { public sealed partial class PaimonSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) { public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.PaimonRoot.FromNotify(ntf), Default.PaimonRoot); return JsonSerializer.Serialize(Outputs.PaimonRoot.FromNotify(ntf), Default.PaimonRoot);

View File

@@ -8,9 +8,9 @@ namespace YaeAchievement.Outputs;
// ReSharper disable PropertyCanBeMadeInitOnly.Global // ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.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; public bool Done => true;
@@ -31,7 +31,7 @@ public class SeelieRoot {
GenerationMode = JsonSourceGenerationMode.Serialization, GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)] )]
public partial class SeelieSerializer : JsonSerializerContext { public sealed partial class SeelieSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) { public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.SeelieRoot.FromNotify(ntf), Default.SeelieRoot); return JsonSerializer.Serialize(Outputs.SeelieRoot.FromNotify(ntf), Default.SeelieRoot);

View File

@@ -9,7 +9,7 @@ namespace YaeAchievement.Outputs;
// ReSharper disable PropertyCanBeMadeInitOnly.Global // ReSharper disable PropertyCanBeMadeInitOnly.Global
#pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global #pragma warning disable CA1822 // ReSharper disable MemberCanBeMadeStatic.Global
public class UApplicationInfo { public sealed class UApplicationInfo {
public string ExportApp => "YaeAchievement"; public string ExportApp => "YaeAchievement";
@@ -21,7 +21,7 @@ public class UApplicationInfo {
} }
public class UAchievementInfo { public sealed class UAchievementInfo {
public uint Id { get; set; } public uint Id { get; set; }
@@ -33,7 +33,7 @@ public class UAchievementInfo {
} }
public class UIAFRoot { public sealed class UIAFRoot {
public UApplicationInfo Info => new (); public UApplicationInfo Info => new ();
@@ -57,7 +57,7 @@ public class UIAFRoot {
GenerationMode = JsonSourceGenerationMode.Serialization, GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)] )]
public partial class UIAFSerializer : JsonSerializerContext { public sealed partial class UIAFSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) { public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot); return JsonSerializer.Serialize(Outputs.UIAFRoot.FromNotify(ntf), Default.UIAFRoot);

View File

@@ -2,7 +2,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Google.Protobuf; using Google.Protobuf;
using Spectre.Console; using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
namespace YaeAchievement.Parsers; namespace YaeAchievement.Parsers;
@@ -14,7 +13,7 @@ public enum AchievementStatus {
RewardTaken, RewardTaken,
} }
public class AchievementItem { public sealed class AchievementItem {
public uint Id { get; init; } public uint Id { get; init; }
public uint TotalProgress { get; init; } public uint TotalProgress { get; init; }
@@ -24,14 +23,14 @@ public class AchievementItem {
} }
public class AchievementAllDataNotify { public sealed class AchievementAllDataNotify {
public List<AchievementItem> AchievementList { get; private init; } = []; public List<AchievementItem> AchievementList { get; private init; } = [];
private static AchievementAllDataNotify? Instance { get; set; } private static AchievementAllDataNotify? Instance { get; set; }
public static bool OnReceive(BinaryReader reader) { public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32()); var bytes = reader.ReadBytes();
CacheFile.Write("achievement_data", bytes); CacheFile.Write("achievement_data", bytes);
Instance = ParseFrom(bytes); Instance = ParseFrom(bytes);
return true; return true;
@@ -62,7 +61,7 @@ public class AchievementAllDataNotify {
} }
dict[tag >> 3] = eStream.ReadUInt32(); dict[tag >> 3] = eStream.ReadUInt32();
} }
if (dict != null) { if (dict is { Count: > 2 }) { // at least 3 fields
data.Add(dict); data.Add(dict);
} }
} catch (InvalidProtocolBufferException) { } catch (InvalidProtocolBufferException) {
@@ -82,7 +81,14 @@ public class AchievementAllDataNotify {
return new AchievementAllDataNotify(); return new AchievementAllDataNotify();
} }
uint tId, sId, iId, currentId, totalId; 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 (tId, var cnt) = data // ↓ 2020-09-15 04:15:14
.GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2); .GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2);
sId = data // FINISHED ↓ ↓ REWARD_TAKEN sId = data // FINISHED ↓ ↓ REWARD_TAKEN
@@ -97,21 +103,14 @@ public class AchievementAllDataNotify {
.Select(g => (FieldIds: g.Key, Count: g.Count())) .Select(g => (FieldIds: g.Key, Count: g.Count()))
.MaxBy(p => p.Count) .MaxBy(p => p.Count)
.FieldIds; .FieldIds;
#if DEBUG #if DEBUG
// ReSharper disable once LocalizableElement // ReSharper disable once LocalizableElement
AnsiConsole.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}"); AnsiConsole.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
#endif #endif
} else { } else {
var info = GlobalVars.AchievementInfo.PbInfo; // ... AnsiConsole.WriteLine(App.WaitMetadataUpdate);
iId = info.Id; Environment.Exit(0);
tId = info.FinishTimestamp; return null!;
sId = info.Status;
totalId = info.TotalProgress;
currentId = info.CurrentProgress;
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
}
} }
return new AchievementAllDataNotify { return new AchievementAllDataNotify {
AchievementList = data.Select(dict => new AchievementItem { AchievementList = data.Select(dict => new AchievementItem {
@@ -122,6 +121,18 @@ public class AchievementAllDataNotify {
FinishTimestamp = dict.GetValueOrDefault(tId), FinishTimestamp = dict.GetValueOrDefault(tId),
}).ToList() }).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));
}
} }
} }
@@ -132,7 +143,7 @@ public class AchievementAllDataNotify {
GenerationMode = JsonSourceGenerationMode.Serialization, GenerationMode = JsonSourceGenerationMode.Serialization,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower
)] )]
public partial class AchievementRawDataSerializer : JsonSerializerContext { public sealed partial class AchievementRawDataSerializer : JsonSerializerContext {
public static string Serialize(AchievementAllDataNotify ntf) { public static string Serialize(AchievementAllDataNotify ntf) {
return JsonSerializer.Serialize(ntf, Default.AchievementAllDataNotify); return JsonSerializer.Serialize(ntf, Default.AchievementAllDataNotify);

View File

@@ -9,7 +9,7 @@ using Spectre.Console;
namespace YaeAchievement.Parsers; namespace YaeAchievement.Parsers;
public class PlayerStoreNotify { public sealed class PlayerStoreNotify {
public uint WeightLimit { get; set; } public uint WeightLimit { get; set; }
@@ -20,7 +20,7 @@ public class PlayerStoreNotify {
public static PlayerStoreNotify Instance { get; } = new (); public static PlayerStoreNotify Instance { get; } = new ();
public static bool OnReceive(BinaryReader reader) { public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32()); var bytes = reader.ReadBytes();
Instance.ParseFrom(bytes); Instance.ParseFrom(bytes);
return true; return true;
} }

View File

@@ -2,7 +2,6 @@
using System.Text; using System.Text;
using Spectre.Console; using Spectre.Console;
using YaeAchievement.Parsers; using YaeAchievement.Parsers;
using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
using static YaeAchievement.Utils; using static YaeAchievement.Utils;
@@ -23,17 +22,36 @@ internal static class Program {
AnsiConsole.WriteLine(App.AnotherInstance); AnsiConsole.WriteLine(App.AnotherInstance);
Environment.Exit(302); 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(); InstallExitHook();
InstallExceptionHook(); InstallExceptionHook();
CheckGenshinIsRunning(); if (GetGameProcess() != null) {
AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
Environment.Exit(-1);
}
await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
AppConfig.Load(args.GetOrNull(0) ?? "auto"); AppConfig.Load(args.GetOrNull(0) ?? "auto");
Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514); Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514);
await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
AchievementAllDataNotify? data = null; AchievementAllDataNotify? data = null;
try { try {
if (CacheFile.TryRead("achievement_data", out var cache)) { if (CacheFile.TryRead("achievement_data", out var cache)) {
@@ -42,10 +60,10 @@ internal static class Program {
} catch (Exception) { /* ignored */ } } catch (Exception) { /* ignored */ }
if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) { if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) {
var prompt = new SelectionPrompt<string>() var prompt = new SelectionPromptCompat<string>()
.Title(App.UsePreviousData) .Title(App.UsePreviousData)
.AddChoices(App.CommonYes, App.CommonNo); .AddChoices(App.CommonYes, App.CommonNo);
if (AnsiConsole.Prompt(prompt) == App.CommonYes) { if (prompt.Prompt() == App.CommonYes) {
Export.Choose(data); Export.Choose(data);
return; return;
} }
@@ -73,6 +91,7 @@ internal static class Program {
internal static void SetupConsole() { internal static void SetupConsole() {
SetQuickEditMode(false); SetQuickEditMode(false);
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
FixTerminalFont();
} }
} }

View File

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

View File

@@ -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>( public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
this IEnumerable<Dictionary<TKey, TValue>> source, this IDictionary<TKey, TValue> dictionary, params TKey[] keys
Func<TValue, bool> condition ) {
) where TKey : notnull { foreach (var key in keys) {
return source.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key)).GroupBy(x => x); 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);
}
}

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

View File

@@ -1,10 +1,32 @@
using System.Runtime.CompilerServices; using System.ComponentModel;
using System.Runtime.CompilerServices;
using Spectre.Console;
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
namespace Google.Protobuf; 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)] [UnsafeAccessor(UnsafeAccessorKind.Method)]
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size); private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);

View File

@@ -11,7 +11,7 @@ using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
namespace YaeAchievement.Utilities; namespace YaeAchievement.Utilities;
public unsafe class GameProcess { public sealed unsafe class GameProcess {
public uint Id { get; } public uint Id { get; }
@@ -29,7 +29,16 @@ public unsafe class GameProcess {
}; };
var wd = Path.GetDirectoryName(path)!; var wd = Path.GetDirectoryName(path)!;
if (!Native.CreateProcess(path, ref cmdLines, null, null, false, flags, null, wd, si, out var pi)) { if (!Native.CreateProcess(path, ref cmdLines, null, null, false, flags, null, wd, si, out var pi)) {
throw new ApplicationException($"CreateProcess fail: {Marshal.GetLastPInvokeErrorMessage()}"); 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; Id = pi.dwProcessId;
Handle = pi.hProcess; Handle = pi.hProcess;
@@ -37,14 +46,14 @@ public unsafe class GameProcess {
Task.Run(() => { Task.Run(() => {
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
OnExit?.Invoke(); OnExit?.Invoke();
}); }).ContinueWith(task => { if (task.IsFaulted) Utils.OnUnhandledException(task.Exception!); });
} }
public void LoadLibrary(string libPath) { public void LoadLibrary(string libPath) {
var hKrnl32 = NativeLibrary.Load("kernel32"); var hKrnl32 = NativeLibrary.Load("kernel32");
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW"); var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
var libPathLen = (uint) libPath.Length * sizeof(char); var libPathLen = (uint) libPath.Length * sizeof(char);
var lpLibPath = Native.VirtualAllocEx(Handle, default, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); var lpLibPath = Native.VirtualAllocEx(Handle, null, libPathLen + 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpLibPath == null) { if (lpLibPath == null) {
throw new ApplicationException($"VirtualAllocEx fail: {Marshal.GetLastPInvokeErrorMessage()}"); throw new ApplicationException($"VirtualAllocEx fail: {Marshal.GetLastPInvokeErrorMessage()}");
} }
@@ -54,7 +63,7 @@ public unsafe class GameProcess {
} }
} }
var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE var lpStartAddress = (delegate*unmanaged[Stdcall]<void*, uint>) mLoadLibraryW; // THREAD_START_ROUTINE
var hThread = Native.CreateRemoteThread(Handle, default, 0, lpStartAddress, lpLibPath, 0); var hThread = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress, lpLibPath, 0);
if (hThread.IsNull) { if (hThread.IsNull) {
var error = Marshal.GetLastPInvokeErrorMessage(); var error = Marshal.GetLastPInvokeErrorMessage();
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE); Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);

View 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];
}
}

View File

@@ -1,5 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Globalization;
using System.IO.Pipes; using System.IO.Pipes;
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@@ -10,16 +10,15 @@ using Windows.Win32.Foundation;
using Windows.Win32.System.Console; using Windows.Win32.System.Console;
using Proto; using Proto;
using Spectre.Console; using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
namespace YaeAchievement; namespace YaeAchievement;
public static class Utils { public static class Utils {
public static HttpClient CHttpClient { get; } = new (new HttpClientHandler { public static HttpClient CHttpClient { get; } = new (new SentryHttpMessageHandler(new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) { })) {
DefaultRequestHeaders = { DefaultRequestHeaders = {
UserAgent = { UserAgent = {
new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2)) new ProductInfoHeaderValue("YaeAchievement", GlobalVars.AppVersion.ToString(2))
@@ -28,20 +27,27 @@ public static class Utils {
}; };
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) { public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
try { try {
return await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache); var data = await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) { } catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
} }
try { try {
return await Task.WhenAny( var data = await Task.WhenAny(
GetFile("https://rin.holohat.work/{0}", path, useCache), GetFile("https://rin.holohat.work/{0}", path, useCache),
GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache) GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
).Unwrap(); ).Unwrap();
} catch (Exception ex) when (ex is SocketException or TaskCanceledException) { transaction.Finish();
return data;
} catch (Exception ex) when (ex is HttpRequestException or SocketException or TaskCanceledException) {
transaction.Finish();
AnsiConsole.WriteLine(App.NetworkError, ex.Message); AnsiConsole.WriteLine(App.NetworkError, ex.Message);
Environment.Exit(-1); Environment.Exit(-1);
} }
return null!; throw new UnreachableException();
static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) { static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey)); using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey));
CacheItem? cache = null; CacheItem? cache = null;
@@ -101,35 +107,42 @@ public static class Utils {
} }
public static async Task CheckUpdate(bool useLocalLib) { public static async Task CheckUpdate(bool useLocalLib) {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version")); try {
var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!; var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) { var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName); if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description); AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
if (versionInfo.EnableAutoUpdate) { AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink)); if (versionInfo.EnableAutoUpdate) {
var tmpPath = Path.GetTempFileName(); var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink));
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe"); var tmpPath = Path.GetTempFileName();
await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) { var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!; await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) {
await srcStream.CopyToAsync(dstStream); 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);
} }
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.EnableLibDownload && !useLocalLib) {
if (versionInfo.ForceUpdate) { var data = await GetBucketFile("schicksal/lic.dll");
Environment.Exit(0); await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
} }
_updateInfo = versionInfo;
} 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 (versionInfo.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
}
_updateInfo = versionInfo;
} }
// ReSharper disable once UnusedMethodReturnValue.Global // ReSharper disable once UnusedMethodReturnValue.Global
@@ -150,18 +163,9 @@ public static class Utils {
} }
} }
internal static void CheckGenshinIsRunning() { internal static Process? GetGameProcess() => Process.GetProcessesByName("YuanShen")
// QueryProcessEvent? .Concat(Process.GetProcessesByName("GenshinImpact"))
var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); .FirstOrDefault(p => File.Exists($"{p.GetFileName()}/../HoYoKProtect.sys"));
foreach (var path in Directory.EnumerateDirectories($"{appdata}/../LocalLow/miHoYo").Where(p => File.Exists($"{p}/info.txt"))) {
try {
using var handle = File.OpenHandle($"{path}/output_log.txt", share: FileShare.None);
} catch (IOException) {
AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
Environment.Exit(301);
}
}
}
private static GameProcess? _proc; private static GameProcess? _proc;
@@ -176,24 +180,26 @@ public static class Utils {
} }
public static void InstallExceptionHook() { public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => { AppDomain.CurrentDomain.UnhandledException += (_, e) => OnUnhandledException((Exception) e.ExceptionObject);
var ex = e.ExceptionObject; }
switch (ex) {
case ApplicationException ex1: public static void OnUnhandledException(Exception ex) {
AnsiConsole.WriteLine(ex1.Message); SentrySdk.CaptureException(ex);
break; switch (ex) {
case SocketException ex2: case ApplicationException ex1:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message); AnsiConsole.WriteLine(ex1.Message);
break; break;
case HttpRequestException ex3: case SocketException ex2:
AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message); AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
break; break;
default: case HttpRequestException ex3:
AnsiConsole.WriteLine(ex.ToString()!); AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
break; break;
} default:
Environment.Exit(-1); AnsiConsole.WriteLine(ex.ToString());
}; break;
}
Environment.Exit(-1);
} }
private static bool _isUnexpectedExit = true; private static bool _isUnexpectedExit = true;
@@ -228,14 +234,30 @@ public static class Utils {
} }
} }
} }
}); }).ContinueWith(task => { if (task.IsFaulted) OnUnhandledException(task.Exception!); });
} }
public static unsafe void SetQuickEditMode(bool enable) { internal static unsafe void SetQuickEditMode(bool enable) {
var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE); var handle = Native.GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
CONSOLE_MODE mode = default; CONSOLE_MODE mode = default;
Native.GetConsoleMode(handle, &mode); Native.GetConsoleMode(handle, &mode);
mode = enable ? mode | CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE : mode &~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE; mode = enable ? mode | CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE : mode &~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
Native.SetConsoleMode(handle, 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.
}
}
} }

View File

@@ -1,7 +1,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata> <metadata>
<id>Yae.Lib</id> <id>Yae.Lib</id>
<version>5.3.3</version> <version>5.3.4</version>
<authors>HolographicHat</authors> <authors>HolographicHat</authors>
<developmentDependency>true</developmentDependency> <developmentDependency>true</developmentDependency>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@@ -369,7 +369,7 @@ namespace
// look for ItemModule.GetBagManagerByStoreType <- mf got inlined in 5.5 // look for ItemModule.GetBagManagerByStoreType <- mf got inlined in 5.5
// we just gon to look for OnPlayerStoreNotify // we just gon to look for OnPlayerStoreNotify
const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 41 83 F8 01"); const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 B8 ? ? ? ? B9 ? ? ? ? 48 0F 45 C1");
std::println("Candidates: {}", candidates.size()); std::println("Candidates: {}", candidates.size());
if (candidates.empty()) if (candidates.empty())
return; return;