spectre console

This commit is contained in:
HolographicHat
2025-04-11 23:57:07 +08:00
parent 0e7be25b23
commit 980a47bf43
11 changed files with 314 additions and 142 deletions

View File

@@ -1,11 +1,11 @@
## Instructions for Use # Instructions for Use
1.Download YaeAchievementLatest Version 1.Download YaeAchievementLatest Version
Click Herehttps://github.com/HolographicHat/YaeAchievement/releases Click Here<https://github.com/HolographicHat/YaeAchievement/releases>
Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that you save this file in a desktop or other easy-to-see folder. Click on the file named "YaeAchievement.exe" in the red box to automatically pop up and download.It is recommended that
you save this file in a desktop or other easy-to-see folder.
![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3) ![Step1](https://github.com/user-attachments/assets/dbe32d1f-3a73-4948-b854-1fb6151ad7f3)

View File

@@ -21,15 +21,19 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Google.Protobuf" Version="3.29.0"/> <PackageReference Include="Google.Protobuf" Version="3.30.2" />
<PackageReference Include="Grpc.Tools" Version="2.67.0"> <PackageReference Include="Grpc.Tools" Version="2.71.0">
<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="Spectre.Console" Version="0.49.1"/> <PackageReference Include="Spectre.Console" Version="0.50.1-preview.0.3" />
<PackageReference Include="Spectre.Console.Analyzer" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -55,6 +59,7 @@
<PropertyGroup> <PropertyGroup>
<CETCompat>false</CETCompat> <CETCompat>false</CETCompat>
<!-- <TrimmerSingleWarn>false</TrimmerSingleWarn>-->
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -86,6 +86,24 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to No.
/// </summary>
internal static string CommonNo {
get {
return ResourceManager.GetString("CommonNo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yes.
/// </summary>
internal static string CommonYes {
get {
return ResourceManager.GetString("CommonYes", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to You need to login genshin impact before exporting.. /// Looks up a localized string similar to You need to login genshin impact before exporting..
/// </summary> /// </summary>
@@ -114,16 +132,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Export to: /// Looks up a localized string similar to Export to:.
///[0] Cocogoat (https://cocogoat.work/achievement, Default)
///[1] Snap.HuTao
///[2] Paimon.moe
///[3] Seelie.me
///[4] Csv file
///[5] Xunkong
///[7] Teyvat Guide
///[8] UIAF JSON File
///Input a number (0-8): .
/// </summary> /// </summary>
internal static string ExportChoose { internal static string ExportChoose {
get { get {
@@ -131,6 +140,87 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to Cocogoat (https://cocogoat.work/achievement).
/// </summary>
internal static string ExportTargetCocogoat {
get {
return ResourceManager.GetString("ExportTargetCocogoat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Csv file.
/// </summary>
internal static string ExportTargetCsv {
get {
return ResourceManager.GetString("ExportTargetCsv", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snap.HuTao.
/// </summary>
internal static string ExportTargetHuTao {
get {
return ResourceManager.GetString("ExportTargetHuTao", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Paimon.moe.
/// </summary>
internal static string ExportTargetPaimon {
get {
return ResourceManager.GetString("ExportTargetPaimon", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seelie.me.
/// </summary>
internal static string ExportTargetSeelie {
get {
return ResourceManager.GetString("ExportTargetSeelie", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teyvat Guide.
/// </summary>
internal static string ExportTargetTeyvatGuide {
get {
return ResourceManager.GetString("ExportTargetTeyvatGuide", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to UIAF JSON File.
/// </summary>
internal static string ExportTargetUIAFJson {
get {
return ResourceManager.GetString("ExportTargetUIAFJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string ExportTargetWxApp1 {
get {
return ResourceManager.GetString("ExportTargetWxApp1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Xunkong.
/// </summary>
internal static string ExportTargetXunkong {
get {
return ResourceManager.GetString("ExportTargetXunkong", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Fail, please contact developer to get help information. /// Looks up a localized string similar to Fail, please contact developer to get help information.
/// </summary> /// </summary>
@@ -320,6 +410,15 @@ namespace YaeAchievement.res {
} }
} }
/// <summary>
/// Looks up a localized string similar to Checking update....
/// </summary>
internal static string UpdateChecking {
get {
return ResourceManager.GetString("UpdateChecking", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Description: /// Looks up a localized string similar to Description:
///{0}. ///{0}.
@@ -358,7 +457,7 @@ namespace YaeAchievement.res {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Use previous fetched data? (yes|no). /// Looks up a localized string similar to Use previous fetched data?.
/// </summary> /// </summary>
internal static string UsePreviousData { internal static string UsePreviousData {
get { get {

View File

@@ -25,16 +25,7 @@
<value>all achievement</value> <value>all achievement</value>
</data> </data>
<data name="ExportChoose" xml:space="preserve"> <data name="ExportChoose" xml:space="preserve">
<value>Export to: <value>Export to:</value>
[0] Cocogoat (https://cocogoat.work/achievement, Default)
[1] Snap.HuTao
[2] Paimon.moe
[3] Seelie.me
[4] Csv file
[5] Xunkong
[7] Teyvat Guide
[8] UIAF JSON File
Input a number (0-8): </value>
</data> </data>
<data name="ExportToCocogoatSuccess" xml:space="preserve"> <data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>Successfully exported to cocogoat.</value> <value>Successfully exported to cocogoat.</value>
@@ -107,7 +98,7 @@ Input a number (0-8): </value>
<value>YaeAchievement ({0})</value> <value>YaeAchievement ({0})</value>
</data> </data>
<data name="UsePreviousData" xml:space="preserve"> <data name="UsePreviousData" xml:space="preserve">
<value>Use previous fetched data? (yes|no)</value> <value>Use previous fetched data?</value>
</data> </data>
<data name="NetworkError" xml:space="preserve"> <data name="NetworkError" xml:space="preserve">
<value>Network error:</value> <value>Network error:</value>
@@ -121,7 +112,6 @@ Input a number (0-8): </value>
<data name="ExceptionNetwork" xml:space="preserve"> <data name="ExceptionNetwork" xml:space="preserve">
<value>Network error ({0}: {1})</value> <value>Network error ({0}: {1})</value>
</data> </data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="GenshinHashError" xml:space="preserve"> <data name="GenshinHashError" xml:space="preserve">
<value>Please update genshin and retry.</value> <value>Please update genshin and retry.</value>
</data> </data>
@@ -137,4 +127,40 @@ Input a number (0-8): </value>
<data name="WaitMetadataUpdate" xml:space="preserve"> <data name="WaitMetadataUpdate" xml:space="preserve">
<value>Please update game and retry.</value> <value>Please update game and retry.</value>
</data> </data>
<data name="UpdateChecking" xml:space="preserve">
<value>Checking update...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>No</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>Cocogoat (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap.HuTao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>Csv file</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>Xunkong</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON File</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -18,17 +18,7 @@
<value>全部成就</value> <value>全部成就</value>
</data> </data>
<data name="ExportChoose" xml:space="preserve"> <data name="ExportChoose" xml:space="preserve">
<value>导出至: <value>导出至:</value>
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
[1] Snap Hutao
[2] Paimon.moe
[3] Seelie.me
[4] 表格文件
[5] 寻空
[6] 原魔工具箱
[7] Teyvat Guide
[8] UIAF JSON 文件
输入一个数字 (0-8): </value>
</data> </data>
<data name="ExportToCocogoatSuccess" xml:space="preserve"> <data name="ExportToCocogoatSuccess" xml:space="preserve">
<value>在浏览器内进行下一步操作</value> <value>在浏览器内进行下一步操作</value>
@@ -101,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>要使用上一次获取到的成就数据吗? (yes|no)</value> <value>要使用上一次获取到的成就数据吗? </value>
</data> </data>
<data name="NetworkError" xml:space="preserve"> <data name="NetworkError" xml:space="preserve">
<value>网络错误: {0}</value> <value>网络错误: {0}</value>
@@ -130,4 +120,40 @@
<data name="WaitMetadataUpdate" xml:space="preserve"> <data name="WaitMetadataUpdate" xml:space="preserve">
<value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value> <value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value>
</data> </data>
<data name="UpdateChecking" xml:space="preserve">
<value>正在检查更新...</value>
</data>
<data name="CommonYes" xml:space="preserve">
<value>是</value>
</data>
<data name="CommonNo" xml:space="preserve">
<value>否</value>
</data>
<data name="ExportTargetCocogoat" xml:space="preserve">
<value>椰羊 (https://cocogoat.work/achievement)</value>
</data>
<data name="ExportTargetHuTao" xml:space="preserve">
<value>Snap Hutao</value>
</data>
<data name="ExportTargetPaimon" xml:space="preserve">
<value>Paimon.moe</value>
</data>
<data name="ExportTargetSeelie" xml:space="preserve">
<value>Seelie.me</value>
</data>
<data name="ExportTargetCsv" xml:space="preserve">
<value>表格文件</value>
</data>
<data name="ExportTargetXunkong" xml:space="preserve">
<value>寻空</value>
</data>
<data name="ExportTargetUIAFJson" xml:space="preserve">
<value>UIAF JSON 文件</value>
</data>
<data name="ExportTargetTeyvatGuide" xml:space="preserve">
<value>Teyvat Guide</value>
</data>
<data name="ExportTargetWxApp1" xml:space="preserve">
<value>原魔工具箱</value>
</data>
</root> </root>

View File

@@ -5,37 +5,40 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.Win32; using Microsoft.Win32;
using Spectre.Console;
using YaeAchievement.Outputs; using YaeAchievement.Outputs;
using YaeAchievement.Parsers; using YaeAchievement.Parsers;
using YaeAchievement.res; using YaeAchievement.res;
// ReSharper disable UnusedMember.Local
namespace YaeAchievement; namespace YaeAchievement;
public static class Export { public static class Export {
public static uint ExportTo { get; set; } = uint.MaxValue; public static int ExportTo { get; set; } = 114514;
public static void Choose(AchievementAllDataNotify data) { public static void Choose(AchievementAllDataNotify data) {
if (ExportTo == uint.MaxValue) { var targets = new Dictionary<string, Action<AchievementAllDataNotify>> {
Console.Write(App.ExportChoose); { App.ExportTargetCocogoat, ToCocogoat },
while (Console.KeyAvailable) { { App.ExportTargetHuTao, ToHuTao },
Console.ReadKey(false); { App.ExportTargetPaimon, ToPaimon },
} { App.ExportTargetSeelie, ToSeelie },
if (!uint.TryParse(Console.ReadLine(), out var num)) num = 0; { App.ExportTargetCsv, ToCSV },
ExportTo = num; { App.ExportTargetXunkong, ToXunkong },
// { App.ExportTargetWxApp1, ToWxApp1 },
{ App.ExportTargetTeyvatGuide, ToTeyvatGuide },
{ App.ExportTargetUIAFJson, ToUIAFJson },
// { "", ToRawJson }
};
Action<AchievementAllDataNotify> action;
if (ExportTo == 114514) {
var prompt = new SelectionPrompt<string>().Title(App.ExportChoose).AddChoices(targets.Keys);
action = targets[AnsiConsole.Prompt(prompt)];
} else {
action = targets.ElementAtOrDefault(ExportTo).Value ?? ToCocogoat;
} }
((Action<AchievementAllDataNotify>) (ExportTo switch { action(data);
1 => ToHuTao,
2 => ToPaimon,
3 => ToSeelie,
4 => ToCSV,
5 => ToXunkong,
6 => ToWxApp1,
7 => ToTeyvatGuide,
8 => ToUIAFJson,
9 => ToRawJson,
_ => ToCocogoat
})).Invoke(data);
} }
private static void ToCocogoat(AchievementAllDataNotify data) { private static void ToCocogoat(AchievementAllDataNotify data) {
@@ -46,17 +49,17 @@ public static class Export {
request.Content = new StringContent(result, Encoding.UTF8, "application/json"); request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request); using var response = Utils.CHttpClient.Send(request);
if (response.StatusCode != HttpStatusCode.Created) { if (response.StatusCode != HttpStatusCode.Created) {
Console.WriteLine(App.ExportToCocogoatFail); AnsiConsole.WriteLine(App.ExportToCocogoatFail);
return; return;
} }
var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var responseJson = JsonSerializer.Deserialize(responseText, CocogoatResponseContext.Default.CocogoatResponse)!; var responseJson = JsonSerializer.Deserialize(responseText, CocogoatResponseContext.Default.CocogoatResponse)!;
var cocogoatUrl = $"https://cocogoat.work/achievement?memo={responseJson.Key}"; var cocogoatUrl = $"https://cocogoat.work/achievement?memo={responseJson.Key}";
Utils.SetQuickEditMode(true); Utils.SetQuickEditMode(true);
Console.WriteLine(cocogoatUrl); AnsiConsole.MarkupLineInterpolated($"[link]{cocogoatUrl}[/]");
if (Utils.ShellOpen(cocogoatUrl)) if (Utils.ShellOpen(cocogoatUrl))
{ {
Console.WriteLine(App.ExportToCocogoatSuccess); AnsiConsole.WriteLine(App.ExportToCocogoatSuccess);
} }
} }
@@ -68,16 +71,16 @@ public static class Export {
request.RequestUri = new Uri("https://api.qyinter.com/achievementRedis"); request.RequestUri = new Uri("https://api.qyinter.com/achievementRedis");
request.Content = new StringContent(result, Encoding.UTF8, "application/json"); request.Content = new StringContent(result, Encoding.UTF8, "application/json");
using var response = Utils.CHttpClient.Send(request); using var response = Utils.CHttpClient.Send(request);
Console.WriteLine(App.ExportToWxApp1Success, id); AnsiConsole.WriteLine(App.ExportToWxApp1Success, id);
} }
private static void ToHuTao(AchievementAllDataNotify data) { private static void ToHuTao(AchievementAllDataNotify data) {
if (CheckWinUIAppScheme("hutao")) { if (CheckWinUIAppScheme("hutao")) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data)); Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("hutao://achievement/import"); Utils.ShellOpen("hutao://achievement/import");
Console.WriteLine(App.ExportToSnapGenshinSuccess); AnsiConsole.WriteLine(App.ExportToSnapGenshinSuccess);
} else { } else {
Console.WriteLine(App.ExportToSnapGenshinNeedUpdate); AnsiConsole.WriteLine(App.ExportToSnapGenshinNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"); Utils.ShellOpen("ms-windows-store://pdp/?productid=9PH4NXJ2JN52");
} }
} }
@@ -86,9 +89,9 @@ public static class Export {
if (CheckWinUIAppScheme("xunkong")) { if (CheckWinUIAppScheme("xunkong")) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data)); Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard"); Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
Console.WriteLine(App.ExportToXunkongSuccess); AnsiConsole.WriteLine(App.ExportToXunkongSuccess);
} else { } else {
Console.WriteLine(App.ExportToXunkongNeedUpdate); AnsiConsole.WriteLine(App.ExportToXunkongNeedUpdate);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12"); Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
} }
} }
@@ -97,9 +100,9 @@ public static class Export {
if (Process.GetProcessesByName("TeyvatGuide").Length != 0) { if (Process.GetProcessesByName("TeyvatGuide").Length != 0) {
Utils.CopyToClipboard(UIAFSerializer.Serialize(data)); Utils.CopyToClipboard(UIAFSerializer.Serialize(data));
Utils.ShellOpen("teyvatguide://import_uiaf?app=Yae"); Utils.ShellOpen("teyvatguide://import_uiaf?app=Yae");
Console.WriteLine(App.ExportToTauriSuccess); AnsiConsole.WriteLine(App.ExportToTauriSuccess);
} else { } else {
Console.WriteLine(App.ExportToTauriFail); AnsiConsole.WriteLine(App.ExportToTauriFail);
Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN"); Utils.ShellOpen("ms-windows-store://pdp/?productid=9NLBNNNBNSJN");
} }
} }
@@ -108,21 +111,21 @@ public static class Export {
private static void ToUIAFJson(AchievementAllDataNotify data) { private static void ToUIAFJson(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json"); var path = Path.GetFullPath($"uiaf-{DateTime.Now:yyyyMMddHHmmss}.json");
if (TryWriteToFile(path, UIAFSerializer.Serialize(data))) { if (TryWriteToFile(path, UIAFSerializer.Serialize(data))) {
Console.WriteLine(App.ExportToFileSuccess, path); AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
} }
} }
private static void ToPaimon(AchievementAllDataNotify data) { private static void ToPaimon(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-paimon.json"); var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-paimon.json");
if (TryWriteToFile(path, PaimonSerializer.Serialize(data))) { if (TryWriteToFile(path, PaimonSerializer.Serialize(data))) {
Console.WriteLine(App.ExportToFileSuccess, path); AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
} }
} }
private static void ToSeelie(AchievementAllDataNotify data) { private static void ToSeelie(AchievementAllDataNotify data) {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-seelie.json"); var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-seelie.json");
if (TryWriteToFile(path, SeelieSerializer.Serialize(data))) { if (TryWriteToFile(path, SeelieSerializer.Serialize(data))) {
Console.WriteLine(App.ExportToFileSuccess, path); AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
} }
} }
@@ -133,7 +136,7 @@ public static class Export {
foreach (var ach in data.AchievementList.OrderBy(a => a.Id)) { foreach (var ach in data.AchievementList.OrderBy(a => a.Id)) {
if (UnusedAchievement.Contains(ach.Id)) continue; if (UnusedAchievement.Contains(ach.Id)) continue;
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) { if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
Console.WriteLine($@"Unable to find {ach.Id} in metadata."); AnsiConsole.WriteLine($@"Unable to find {ach.Id} in metadata.");
continue; continue;
} }
var finishAt = ""; var finishAt = "";
@@ -156,7 +159,7 @@ public static class Export {
})); }));
var path = Path.GetFullPath($"achievement-{DateTime.Now:yyyyMMddHHmmss}.csv"); var path = Path.GetFullPath($"achievement-{DateTime.Now:yyyyMMddHHmmss}.csv");
if (TryWriteToFile(path, $"\uFEFF{string.Join("\n", output)}")) { if (TryWriteToFile(path, $"\uFEFF{string.Join("\n", output)}")) {
Console.WriteLine(App.ExportToFileSuccess, path); AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}"); Process.Start("explorer.exe", $"{Path.GetDirectoryName(path)}");
} }
} }
@@ -165,7 +168,7 @@ public static class Export {
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json"); var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
var text = AchievementRawDataSerializer.Serialize(data); var text = AchievementRawDataSerializer.Serialize(data);
if (TryWriteToFile(path, text)) { if (TryWriteToFile(path, text)) {
Console.WriteLine(App.ExportToFileSuccess, path); AnsiConsole.WriteLine(App.ExportToFileSuccess, path);
} }
} }
@@ -192,7 +195,7 @@ public static class Export {
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) { public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
// ReSharper disable once LocalizableElement // ReSharper disable once LocalizableElement
Console.WriteLine($"{msg}: {ex.Message}"); AnsiConsole.WriteLine($"{msg}: {ex.Message}");
return ex.NativeErrorCode; return ex.NativeErrorCode;
} }
@@ -201,7 +204,7 @@ public static class Export {
File.WriteAllText(path, contents); File.WriteAllText(path, contents);
return true; return true;
} catch (UnauthorizedAccessException) { } catch (UnauthorizedAccessException) {
Console.WriteLine(App.NoWritePermission, path); AnsiConsole.WriteLine(App.NoWritePermission, path);
return false; return false;
} }
} }

View File

@@ -4,11 +4,6 @@ using Proto;
namespace YaeAchievement; namespace YaeAchievement;
// ReSharper disable InconsistentNaming
// ReSharper disable ConvertToConstant.Global
// ReSharper disable FieldCanBeMadeReadOnly.Global
// ReSharper disable once MemberCanBePrivate.Global
public static class GlobalVars { public static class GlobalVars {
public static bool PauseOnExit { get; set; } = true; public static bool PauseOnExit { get; set; } = true;
@@ -24,8 +19,6 @@ public static class GlobalVars {
public const string AppVersionName = "5.3"; public const string AppVersionName = "5.3";
public const string PipeName = "YaeAchievementPipe"; public const string PipeName = "YaeAchievementPipe";
public const string RinBucketHost = "https://rin.holohat.work";
public const string SakuraBucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
[field:MaybeNull] [field:MaybeNull]
public static AchievementInfo AchievementInfo => public static AchievementInfo AchievementInfo =>

View File

@@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Google.Protobuf; using Google.Protobuf;
using Spectre.Console;
using YaeAchievement.res; using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
@@ -73,7 +74,7 @@ public class AchievementAllDataNotify {
} }
} catch (InvalidProtocolBufferException) { } catch (InvalidProtocolBufferException) {
// ReSharper disable once LocalizableElement // ReSharper disable once LocalizableElement
Console.WriteLine("Parse failed"); AnsiConsole.WriteLine("Parse failed");
File.WriteAllBytes("achievement_raw_data.bin", bytes); File.WriteAllBytes("achievement_raw_data.bin", bytes);
Environment.Exit(0); Environment.Exit(0);
} }
@@ -98,7 +99,7 @@ public class AchievementAllDataNotify {
.FieldIds; .FieldIds;
#if DEBUG #if DEBUG
// ReSharper disable once LocalizableElement // ReSharper disable once LocalizableElement
Console.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; // ... var info = GlobalVars.AchievementInfo.PbInfo; // ...
@@ -108,7 +109,7 @@ public class AchievementAllDataNotify {
totalId = info.TotalProgress; totalId = info.TotalProgress;
currentId = info.CurrentProgress; currentId = info.CurrentProgress;
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) { if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
Console.WriteLine(App.WaitMetadataUpdate); AnsiConsole.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0); Environment.Exit(0);
} }
} }

View File

@@ -1,5 +1,6 @@
using Google.Protobuf; using Google.Protobuf;
using Proto; using Proto;
using Spectre.Console;
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
// ReSharper disable CollectionNeverQueried.Global // ReSharper disable CollectionNeverQueried.Global
@@ -51,7 +52,7 @@ public class PlayerStoreNotify {
} }
} catch (InvalidProtocolBufferException) { } catch (InvalidProtocolBufferException) {
// ReSharper disable once LocalizableElement // ReSharper disable once LocalizableElement
Console.WriteLine("Parse failed"); AnsiConsole.WriteLine("Parse failed");
File.WriteAllBytes("store_raw_data.bin", bytes); File.WriteAllBytes("store_raw_data.bin", bytes);
Environment.Exit(0); Environment.Exit(0);
} }

View File

@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using Spectre.Console;
using YaeAchievement.Parsers; using YaeAchievement.Parsers;
using YaeAchievement.res; using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
@@ -11,8 +12,13 @@ internal static class Program {
public static async Task Main(string[] args) { public static async Task Main(string[] args) {
AnsiConsole.WriteLine(@"----------------------------------------------------");
AnsiConsole.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
AnsiConsole.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
AnsiConsole.WriteLine(@"----------------------------------------------------");
if (!new Mutex(true, @"Global\YaeMiku~uwu").WaitOne(0, false)) { if (!new Mutex(true, @"Global\YaeMiku~uwu").WaitOne(0, false)) {
Console.WriteLine(App.AnotherInstance); AnsiConsole.WriteLine(App.AnotherInstance);
Environment.Exit(302); Environment.Exit(302);
} }
@@ -21,15 +27,10 @@ internal static class Program {
CheckGenshinIsRunning(); CheckGenshinIsRunning();
Console.WriteLine(@"----------------------------------------------------");
Console.WriteLine(App.AppBanner, GlobalVars.AppVersionName);
Console.WriteLine(@"https://github.com/HolographicHat/YaeAchievement");
Console.WriteLine(@"----------------------------------------------------");
AppConfig.Load(args.GetOrNull(0) ?? "auto"); AppConfig.Load(args.GetOrNull(0) ?? "auto");
Export.ExportTo = ToUIntOrNull(args.GetOrNull(1)) ?? uint.MaxValue; Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514);
await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2))); await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
AchievementAllDataNotify? data = null; AchievementAllDataNotify? data = null;
try { try {
@@ -39,8 +40,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) {
Console.WriteLine(App.UsePreviousData); var prompt = new SelectionPrompt<string>()
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") { .Title(App.UsePreviousData)
.AddChoices(App.CommonYes, App.CommonNo);
if (AnsiConsole.Prompt(prompt) == App.CommonYes) {
Export.Choose(data); Export.Choose(data);
return; return;
} }
@@ -51,7 +54,7 @@ internal static class Program {
{ 2, PlayerStoreNotify.OnReceive }, { 2, PlayerStoreNotify.OnReceive },
{ 100, PlayerPropNotify.OnReceive }, { 100, PlayerPropNotify.OnReceive },
}, () => { }, () => {
#if DEBUG #if DEBUG_EX
PlayerPropNotify.OnFinish(); PlayerPropNotify.OnFinish();
File.WriteAllText("store_data.json", JsonSerializer.Serialize(PlayerStoreNotify.Instance, new JsonSerializerOptions { File.WriteAllText("store_data.json", JsonSerializer.Serialize(PlayerStoreNotify.Instance, new JsonSerializerOptions {
WriteIndented = true, WriteIndented = true,

View File

@@ -9,6 +9,7 @@ using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.System.Console; using Windows.Win32.System.Console;
using Proto; using Proto;
using Spectre.Console;
using YaeAchievement.res; using YaeAchievement.res;
using YaeAchievement.Utilities; using YaeAchievement.Utilities;
@@ -28,31 +29,36 @@ 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) {
try { try {
return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost)); return await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
} catch (Exception e) when(e is SocketException or TaskCanceledException) { } catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
Console.WriteLine(App.NetworkError, e.Message);
Environment.Exit(-1);
return null!;
} }
async Task<byte[]> GetFile(string host) { try {
using var msg = new HttpRequestMessage(); return await Task.WhenAny(
msg.Method = HttpMethod.Get; GetFile("https://rin.holohat.work/{0}", path, useCache),
msg.RequestUri = new Uri($"{host}/{path}"); GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
).Unwrap();
} catch (Exception ex) when (ex is SocketException or TaskCanceledException) {
AnsiConsole.WriteLine(App.NetworkError, ex.Message);
Environment.Exit(-1);
}
return null!;
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; CacheItem? cache = null;
if (useCache && CacheFile.TryRead(path, out cache)) { if (useCache && CacheFile.TryRead(objectKey, out cache)) {
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}"); reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
} }
using var response = await CHttpClient.SendAsync(msg); using var response = await CHttpClient.SendAsync(reqwest);
if (cache != null && response.StatusCode == HttpStatusCode.NotModified) { if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
return cache.Content.ToByteArray(); return cache.Content.ToByteArray();
} }
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var responseBytes = await response.Content.ReadAsByteArrayAsync(); var bytes = await response.Content.ReadAsByteArrayAsync();
if (useCache) { if (useCache) {
var etag = response.Headers.ETag!.Tag; var etag = response.Headers.ETag!.Tag;
CacheFile.Write(path, responseBytes, etag); CacheFile.Write(objectKey, bytes, etag);
} }
return responseBytes; return bytes;
} }
} }
@@ -60,12 +66,12 @@ public static class Utils {
return array.Length > index ? array[index] : null; return array.Length > index ? array[index] : null;
} }
public static uint? ToUIntOrNull(string? value) { public static int ToIntOrDefault(string? value, int defaultValue = 0) {
return value != null ? uint.TryParse(value, out var result) ? result : null : null; return value != null && int.TryParse(value, out var result) ? result : defaultValue;
} }
public static bool ToBooleanOrFalse(string? value) { public static bool ToBooleanOrDefault(string? value, bool defaultValue = false) {
return value != null && bool.TryParse(value, out var result) && result; return value != null && bool.TryParse(value, out var result) ? result : defaultValue;
} }
public static unsafe void CopyToClipboard(string text) { public static unsafe void CopyToClipboard(string text) {
@@ -86,35 +92,44 @@ public static class Utils {
// ReSharper disable once NotAccessedField.Local // ReSharper disable once NotAccessedField.Local
private static UpdateInfo _updateInfo = null!; private static UpdateInfo _updateInfo = null!;
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static Task<T> StartSpinnerAsync<T>(string status, Func<StatusContext, Task<T>> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
}
public static async Task CheckUpdate(bool useLocalLib) { public static async Task CheckUpdate(bool useLocalLib) {
var info = UpdateInfo.Parser.ParseFrom(await GetBucketFile("schicksal/version"))!; var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
if (GlobalVars.AppVersionCode < info.VersionCode) { var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
Console.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, info.VersionName); if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
Console.WriteLine(App.UpdateDescription, info.Description); AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
if (info.EnableAutoUpdate) { AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
Console.WriteLine(App.UpdateDownloading); if (versionInfo.EnableAutoUpdate) {
var newBin = await StartSpinnerAsync(App.UpdateDownloading, _ => GetBucketFile(versionInfo.PackageLink));
var tmpPath = Path.GetTempFileName(); var tmpPath = Path.GetTempFileName();
await File.WriteAllBytesAsync(tmpPath, await GetBucketFile(info.PackageLink));
var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe"); var updaterPath = Path.Combine(GlobalVars.DataPath, "update.exe");
await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) { await using (var dstStream = File.Open($"{GlobalVars.DataPath}/update.exe", FileMode.Create)) {
await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!; await using var srcStream = typeof(Program).Assembly.GetManifestResourceStream("updater")!;
await srcStream.CopyToAsync(dstStream); await srcStream.CopyToAsync(dstStream);
} }
await File.WriteAllBytesAsync(tmpPath, newBin);
ShellOpen(updaterPath, $"{Environment.ProcessId} \"{tmpPath}\""); ShellOpen(updaterPath, $"{Environment.ProcessId} \"{tmpPath}\"");
await Task.Delay(1919810); await StartSpinnerAsync(App.UpdateChecking, _ => Task.Delay(1919810));
GlobalVars.PauseOnExit = false; GlobalVars.PauseOnExit = false;
Environment.Exit(0); Environment.Exit(0);
} }
Console.WriteLine(App.DownloadLink, info.PackageLink); AnsiConsole.MarkupLine($"[link]{App.DownloadLink}[/]", versionInfo.PackageLink);
if (info.ForceUpdate) { if (versionInfo.ForceUpdate) {
Environment.Exit(0); Environment.Exit(0);
} }
} }
if (info.EnableLibDownload && !useLocalLib) { if (versionInfo.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll"); var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
} }
_updateInfo = info; _updateInfo = versionInfo;
} }
// ReSharper disable once UnusedMethodReturnValue.Global // ReSharper disable once UnusedMethodReturnValue.Global
@@ -142,7 +157,7 @@ public static class Utils {
try { try {
using var handle = File.OpenHandle($"{path}/output_log.txt", share: FileShare.None); using var handle = File.OpenHandle($"{path}/output_log.txt", share: FileShare.None);
} catch (IOException) { } catch (IOException) {
Console.WriteLine(App.GenshinIsRunning, 0); AnsiConsole.WriteLine(App.GenshinIsRunning, 0);
Environment.Exit(301); Environment.Exit(301);
} }
} }
@@ -154,7 +169,7 @@ public static class Utils {
AppDomain.CurrentDomain.ProcessExit += (_, _) => { AppDomain.CurrentDomain.ProcessExit += (_, _) => {
_proc?.Terminate(0); _proc?.Terminate(0);
if (GlobalVars.PauseOnExit) { if (GlobalVars.PauseOnExit) {
Console.WriteLine(App.PressKeyToExit); AnsiConsole.WriteLine(App.PressKeyToExit);
Console.ReadKey(); Console.ReadKey();
} }
}; };
@@ -165,16 +180,16 @@ public static class Utils {
var ex = e.ExceptionObject; var ex = e.ExceptionObject;
switch (ex) { switch (ex) {
case ApplicationException ex1: case ApplicationException ex1:
Console.WriteLine(ex1.Message); AnsiConsole.WriteLine(ex1.Message);
break; break;
case SocketException ex2: case SocketException ex2:
Console.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message); AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(SocketException), ex2.Message);
break; break;
case HttpRequestException ex3: case HttpRequestException ex3:
Console.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message); AnsiConsole.WriteLine(App.ExceptionNetwork, nameof(HttpRequestException), ex3.Message);
break; break;
default: default:
Console.WriteLine(ex.ToString()); AnsiConsole.WriteLine(ex.ToString()!);
break; break;
} }
Environment.Exit(-1); Environment.Exit(-1);
@@ -189,13 +204,13 @@ public static class Utils {
_proc.OnExit += () => { _proc.OnExit += () => {
if (_isUnexpectedExit) { if (_isUnexpectedExit) {
_proc = null; _proc = null;
Console.WriteLine(App.GameProcessExit); AnsiConsole.WriteLine(App.GameProcessExit);
Environment.Exit(114514); Environment.Exit(114514);
} }
}; };
_proc.LoadLibrary(GlobalVars.LibFilePath); _proc.LoadLibrary(GlobalVars.LibFilePath);
_proc.ResumeMainThread(); _proc.ResumeMainThread();
Console.WriteLine(App.GameLoading, _proc.Id); AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
Task.Run(() => { Task.Run(() => {
using var stream = new NamedPipeServerStream(GlobalVars.PipeName); using var stream = new NamedPipeServerStream(GlobalVars.PipeName);
using var reader = new BinaryReader(stream); using var reader = new BinaryReader(stream);