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)
1. [胡桃工具箱](https://github.com/DGP-Studio/Snap.HuTao)
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)
## 常见问题
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)
A: 不要把软件和原神主程序放一起

View File

@@ -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/)
@@ -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)
## 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)
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)
1. [胡桃ツールボックス](https://github.com/DGP-Studio/Snap.HuTao)
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)
## よくある質問
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)
A: ソフトウェアを原神のディレクトリに配置しないでください。

View File

@@ -8,19 +8,7 @@
![1](https://github.com/user-attachments/assets/8b98c018-b179-4681-992d-367a0f522dae)
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.打开主程序所需的操作以及成就导出的选择
2.打开主程序所需的操作以及成就导出的选择
双击在第一步下载的名称为“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)
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
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)
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. メインプログラムを開くための操作と実績エクスポートのオプション
最初のステップでダウンロードした「YaeAchievement.exe」という名前のファイルをダブルクリックして開くと、原神が起動していることを示します。以下の図のように表示されます。

View File

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

View File

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

View File

@@ -105,7 +105,7 @@ 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 Please launch GenshinImpact to continue..
/// </summary>
internal static string ConfigNeedStartGenshin {
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>
/// Looks up a localized string similar to Reward not taken.
/// </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>
/// Looks up a localized string similar to Checking update....
/// </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>
/// Looks up a localized string similar to Has update: {0} =&gt; {1}.
/// </summary>

View File

@@ -61,7 +61,7 @@
<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>
@@ -163,4 +163,22 @@
<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>
</root>

View File

@@ -18,7 +18,7 @@
<value>全部成就</value>
</data>
<data name="ExportChoose" xml:space="preserve">
<value>要导出到哪里?(键盘上下键移动光标,键盘回车键选择)</value>
<value>要导出到哪里?</value>
</data>
<data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>在浏览器内进行下一步操作</value>
@@ -54,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>
@@ -91,7 +91,7 @@
<value>YaeAchievement - 原神成就导出工具 ({0})</value>
</data>
<data name="UsePreviousData" xml:space="preserve">
<value>要使用上一次获取到的成就数据吗?(键盘上下键移动光标,键盘回车键选择)</value>
<value>要使用上一次获取到的成就数据吗?</value>
</data>
<data name="NetworkError" xml:space="preserve">
<value>网络错误: {0}</value>
@@ -156,4 +156,22 @@
<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>
</root>

View File

@@ -1,5 +1,8 @@
using System.Text.RegularExpressions;
using YaeAchievement.res;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Spectre.Console;
using YaeAchievement.Utilities;
namespace YaeAchievement;
@@ -13,26 +16,66 @@ 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();
}
if (CacheFile.TryRead("genshin_impact_game_path", out var cache)) {
var path = cache.Content.ToStringUtf8();
if (path != null && File.Exists(path)) {
GamePath = path;
return;
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(GamePath);
if (stream.Read(buffer) == buffer.Length) {
var hash = Convert.ToHexString(MD5.HashData(buffer));
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{hash}"));
}
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
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);
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) {

View File

@@ -1,5 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
@@ -8,7 +7,7 @@ using Microsoft.Win32;
using Spectre.Console;
using YaeAchievement.Outputs;
using YaeAchievement.Parsers;
using YaeAchievement.res;
using YaeAchievement.Utilities;
// ReSharper disable UnusedMember.Local
@@ -33,8 +32,8 @@ public static class Export {
};
Action<AchievementAllDataNotify> action;
if (ExportTo == 114514) {
var prompt = new SelectionPrompt<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[AnsiConsole.Prompt(prompt)];
var prompt = new SelectionPromptCompat<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[prompt.Prompt()];
} else {
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!;
@@ -223,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,
@@ -231,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;

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 Proto;
@@ -15,8 +18,8 @@ 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 = 236;
public const string AppVersionName = "5.4";
public const uint AppVersionCode = 238;
public const string AppVersionName = "5.6";
public const string PipeName = "YaeAchievementPipe";

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
using System.Text.Json.Serialization;
using Google.Protobuf;
using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement.Parsers;
@@ -14,7 +13,7 @@ public enum AchievementStatus {
RewardTaken,
}
public class AchievementItem {
public sealed class AchievementItem {
public uint Id { 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; } = [];
private static AchievementAllDataNotify? Instance { get; set; }
public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32());
var bytes = reader.ReadBytes();
CacheFile.Write("achievement_data", bytes);
Instance = ParseFrom(bytes);
return true;
@@ -62,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) {
@@ -82,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
@@ -97,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
AnsiConsole.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
#endif
#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))) {
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
}
AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
return null!;
}
return new AchievementAllDataNotify {
AchievementList = data.Select(dict => new AchievementItem {
@@ -122,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));
}
}
}
@@ -132,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);

View File

@@ -9,7 +9,7 @@ using Spectre.Console;
namespace YaeAchievement.Parsers;
public class PlayerStoreNotify {
public sealed class PlayerStoreNotify {
public uint WeightLimit { get; set; }
@@ -20,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;
}

View File

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

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

View File

@@ -11,7 +11,7 @@ using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
namespace YaeAchievement.Utilities;
public unsafe class GameProcess {
public sealed unsafe class GameProcess {
public uint Id { get; }
@@ -29,7 +29,16 @@ public unsafe class GameProcess {
};
var wd = Path.GetDirectoryName(path)!;
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;
Handle = pi.hProcess;
@@ -37,14 +46,14 @@ public unsafe class GameProcess {
Task.Run(() => {
Native.WaitForSingleObject(Handle, 0xFFFFFFFF); // INFINITE
OnExit?.Invoke();
});
}).ContinueWith(task => { if (task.IsFaulted) Utils.OnUnhandledException(task.Exception!); });
}
public void LoadLibrary(string libPath) {
var hKrnl32 = NativeLibrary.Load("kernel32");
var mLoadLibraryW = NativeLibrary.GetExport(hKrnl32, "LoadLibraryW");
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) {
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 hThread = Native.CreateRemoteThread(Handle, default, 0, lpStartAddress, lpLibPath, 0);
var hThread = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress, lpLibPath, 0);
if (hThread.IsNull) {
var error = Marshal.GetLastPInvokeErrorMessage();
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.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
@@ -10,16 +10,15 @@ using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Proto;
using Spectre.Console;
using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement;
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
}) {
})) {
DefaultRequestHeaders = {
UserAgent = {
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) {
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
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) {
}
try {
return await Task.WhenAny(
var data = await Task.WhenAny(
GetFile("https://rin.holohat.work/{0}", path, useCache),
GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
).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);
Environment.Exit(-1);
}
return null!;
throw new UnreachableException();
static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey));
CacheItem? cache = null;
@@ -101,35 +107,42 @@ public static class Utils {
}
public static async Task CheckUpdate(bool useLocalLib) {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = 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);
try {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = 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);
}
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);
if (versionInfo.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll");
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
@@ -150,18 +163,9 @@ public static class Utils {
}
}
internal static void CheckGenshinIsRunning() {
// QueryProcessEvent?
var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
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);
}
}
}
internal static Process? GetGameProcess() => Process.GetProcessesByName("YuanShen")
.Concat(Process.GetProcessesByName("GenshinImpact"))
.FirstOrDefault(p => File.Exists($"{p.GetFileName()}/../HoYoKProtect.sys"));
private static GameProcess? _proc;
@@ -176,24 +180,26 @@ public static class Utils {
}
public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
var ex = e.ExceptionObject;
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);
};
AppDomain.CurrentDomain.UnhandledException += (_, e) => OnUnhandledException((Exception) e.ExceptionObject);
}
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;
@@ -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);
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.
}
}
}

View File

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

View File

@@ -369,7 +369,7 @@ namespace
// 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");
const auto candidates = Util::PatternScanAll(il2cppSection, "41 83 F8 02 B8 ? ? ? ? B9 ? ? ? ? 48 0F 45 C1");
std::println("Candidates: {}", candidates.size());
if (candidates.empty())
return;