mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-10 08:28:12 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e94d67d0b | ||
|
|
7dafd95099 | ||
|
|
76c20407ea | ||
|
|
b79b82ec10 | ||
|
|
07fe60a092 | ||
|
|
7fa2fbac25 | ||
|
|
28ffa6fb1a | ||
|
|
4c2cb28313 | ||
|
|
2f1a5ad99e | ||
|
|
9b0c384d4b | ||
|
|
b2111db4eb | ||
|
|
2442264224 | ||
|
|
b596cad02e | ||
|
|
30a0189f5e | ||
|
|
f47fd234b4 | ||
|
|
7d306a60c9 | ||
|
|
10dd03335f | ||
|
|
41863c32f7 | ||
|
|
4c3e9d8e50 | ||
|
|
9d60cda4c7 | ||
|
|
e0c836e55d | ||
|
|
02b4d9df0b |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,3 +4,10 @@ obj/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.vs/
|
||||
25
README.md
25
README.md
@@ -2,6 +2,29 @@
|
||||
|
||||
# YaeAchievement
|
||||
|
||||
    
|
||||
    
|
||||
|
||||
</div>
|
||||
|
||||
- 支持导出所有类别的成就
|
||||
- 支持官服,渠道服与国际服
|
||||
- 支持导出至[椰羊](https://cocogoat.work/achievement)、[SnapGenshin](https://github.com/DGP-Studio/Snap.Genshin)、[Paimon.moe](https://paimon.moe/achievement/)、[Seelie.me](https://seelie.me/achievements)、[寻空](https://github.com/xunkong/xunkong)和表格文件(csv)
|
||||
- 没有窗口大小、游戏语言等要求
|
||||
|
||||
## 使用说明
|
||||
第一次打开需要先设置原神主程序(YuanShen.exe/GenshinImpact.exe)所在路径
|
||||

|
||||
设置完毕后,等待原神自动启动并退出
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/YaeAchievement/releases/latest)
|
||||
|
||||
## 问题反馈
|
||||
[issues](https://github.com/HolographicHat/YaeAchievement/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## 常见问题
|
||||
0. Q: 打不开
|
||||
A: 安装 [.NET Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.7-windows-x64-installer)
|
||||
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
A: 不要把软件和原神主程序放一起
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>res\app.manifest</ApplicationManifest>
|
||||
<AssemblyVersion>2.0.0</AssemblyVersion>
|
||||
@@ -13,8 +14,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.21.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.21.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
<ClInclude Include="src\il2cpp-functions.h" />
|
||||
<ClInclude Include="src\il2cpp-types.h" />
|
||||
<ClInclude Include="src\il2cpp-init.h" />
|
||||
<ClInclude Include="src\il2cpp-unity-functions.h" />
|
||||
<ClInclude Include="src\pch.h" />
|
||||
<ClInclude Include="src\util.h" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@ using std::to_string;
|
||||
HWND unityWnd = 0;
|
||||
HANDLE hPipe = 0;
|
||||
|
||||
std::set<UINT16> PacketWhitelist = { 172, 198, 112, 2676, 7, 21 }; // ping, token, loginreq
|
||||
std::set<UINT16> PacketWhitelist = { 172, 198, 112, 2676, 7, 21, 135 }; // ping, token, loginreq
|
||||
|
||||
bool OnPacket(KcpPacket* pkt) {
|
||||
if (pkt->data == nullptr) return true;
|
||||
@@ -22,12 +22,13 @@ bool OnPacket(KcpPacket* pkt) {
|
||||
return true;
|
||||
}
|
||||
if (!PacketWhitelist.contains(ReadMapped<UINT16>(data->vector, 2))) {
|
||||
#ifdef _DEBUG
|
||||
//ifdef _DEBUG
|
||||
printf("Blocked cmdid: %d\n", ReadMapped<UINT16>(data->vector, 2));
|
||||
#endif
|
||||
//endif
|
||||
delete[] data;
|
||||
return false;
|
||||
}
|
||||
printf("Passed cmdid: %d\n", ReadMapped<UINT16>(data->vector, 2));
|
||||
if (ReadMapped<UINT16>(data->vector, 2) == 2676) {
|
||||
auto headLength = ReadMapped<UINT16>(data->vector, 4);
|
||||
auto dataLength = ReadMapped<UINT32>(data->vector, 6);
|
||||
@@ -47,6 +48,12 @@ namespace Hook {
|
||||
return OnPacket(pkt) ? CALL_ORIGIN(Kcp_Send, client, pkt, method) : 0;
|
||||
}
|
||||
|
||||
void MonoLoginMainPage__set_version(void* obj, Il2CppString* value, void* method) {
|
||||
auto version = IlStringToString(value);
|
||||
value = string_new(version + " YaeAchievement");
|
||||
CALL_ORIGIN(MonoLoginMainPage__set_version, obj, value, method);
|
||||
}
|
||||
|
||||
bool Kcp_Recv(void* client, ClientKcpEvent* evt, void* method) {
|
||||
auto result = CALL_ORIGIN(Kcp_Recv, client, evt, method);
|
||||
if (result == 0 || evt->fields.type != KcpEventType::EventRecvMsg) {
|
||||
@@ -54,6 +61,17 @@ namespace Hook {
|
||||
}
|
||||
return OnPacket(evt->fields.packet) ? result : false;
|
||||
}
|
||||
|
||||
std::map<INT, UINT> signatures;
|
||||
|
||||
ByteArray* UnityEngine_RecordUserData(INT type) {
|
||||
if (signatures.count(type)) {
|
||||
return GCHandle_GetObject<ByteArray>(signatures[type]);
|
||||
}
|
||||
auto result = CALL_ORIGIN(UnityEngine_RecordUserData, type);
|
||||
signatures[type] = GCHandle_New(result, true);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void Run(HMODULE* phModule) {
|
||||
@@ -66,8 +84,13 @@ void Run(HMODULE* phModule) {
|
||||
Sleep(1000);
|
||||
}
|
||||
InitIL2CPP();
|
||||
HookManager::install(Genshin::UnityEngine_RecordUserData, Hook::UnityEngine_RecordUserData);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Genshin::Application_RecordUserData(i, nullptr);
|
||||
}
|
||||
HookManager::install(Genshin::Kcp_Send, Hook::Kcp_Send);
|
||||
HookManager::install(Genshin::Kcp_Recv, Hook::Kcp_Recv);
|
||||
HookManager::install(Genshin::MonoLoginMainPage__set_version, Hook::MonoLoginMainPage__set_version);
|
||||
hPipe = CreateFile(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (hPipe == INVALID_HANDLE_VALUE) {
|
||||
Win32ErrorDialog(1001);
|
||||
|
||||
@@ -12,3 +12,9 @@ namespace Genshin {
|
||||
#include "il2cpp-functions.h"
|
||||
}
|
||||
#undef DO_APP_FUNC
|
||||
|
||||
#define DO_UNI_FUNC(ca, oa, r, n, p) extern r (*n) p
|
||||
namespace Genshin {
|
||||
#include "il2cpp-unity-functions.h"
|
||||
}
|
||||
#undef DO_UNI_FUNC
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using namespace Genshin;
|
||||
|
||||
DO_APP_FUNC(0x05E24240, 0x04EA1150, Il2CppString*, Convert_ToBase64String, (ByteArray* value, int offset, int length, void* method));
|
||||
DO_APP_FUNC(0x018280A0, 0x018293F0, void, Packet_Xor, (ByteArray** data, int length, void* method));
|
||||
DO_APP_FUNC(0x05254960, 0x052544E0, Il2CppString*, Convert_ToBase64String, (ByteArray* value, int offset, int length, void* method));
|
||||
DO_APP_FUNC(0x020127B0, 0x02012D40, void, Packet_Xor, (ByteArray** data, int length, void* method));
|
||||
|
||||
DO_APP_FUNC(0x0193BA70, 0x0193C7D0, int, Kcp_Send, (void* client, KcpPacket* pkt, void* method));
|
||||
DO_APP_FUNC(0x029EF820, 0x029F05C0, bool, Kcp_Recv, (void* client, ClientKcpEvent* evt, void* method));
|
||||
DO_APP_FUNC(0X01AD8E40, 0x01AD9740, void, MonoLoginMainPage__set_version, (void* obj, Il2CppString* value, void* method));
|
||||
DO_APP_FUNC(0x05C25AC0, 0x05C25E60, ByteArray*, Application_RecordUserData, (int32_t nType, void* method));
|
||||
|
||||
DO_APP_FUNC(0x015C19D0, 0x015C2150, int, Kcp_Send, (void* client, KcpPacket* pkt, void* method));
|
||||
DO_APP_FUNC(0x02CF31D0, 0x02CF33A0, bool, Kcp_Recv, (void* client, ClientKcpEvent* evt, void* method));
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace Genshin {
|
||||
}
|
||||
#undef DO_APP_FUNC
|
||||
|
||||
#define DO_UNI_FUNC(ca, oa, r, n, p) r (*n) p
|
||||
namespace Genshin {
|
||||
#include "il2cpp-unity-functions.h"
|
||||
}
|
||||
#undef DO_UNI_FUNC
|
||||
|
||||
using std::string;
|
||||
|
||||
UINT64 GetAddressByExports(HMODULE base, const char* name) {
|
||||
@@ -22,13 +28,17 @@ UINT64 GetAddressByExports(HMODULE base, const char* name) {
|
||||
void InitIL2CPP() {
|
||||
TCHAR szFileName[MAX_PATH];
|
||||
GetModuleFileName(NULL, szFileName, MAX_PATH);
|
||||
auto isCN = string(szFileName).contains("YuanShen.exe");
|
||||
auto isCN = strstr(szFileName, "YuanShen.exe");//string(szFileName).contains();
|
||||
auto hBase = GetModuleHandle("UserAssembly.dll");
|
||||
auto bAddr = (UINT64)hBase;
|
||||
auto cAddr = (UINT64)GetModuleHandle("UnityPlayer.dll");
|
||||
#define DO_API(r, n, p) n = (r (*) p) GetAddressByExports(hBase, #n);
|
||||
#include "il2cpp-api-functions.h"
|
||||
#undef DO_API
|
||||
#define DO_APP_FUNC(ca, oa, r, n, p) n = (r (*) p)(bAddr + (isCN ? ca : oa))
|
||||
#include "il2cpp-functions.h"
|
||||
#undef DO_APP_FUNC
|
||||
#define DO_UNI_FUNC(ca, oa, r, n, p) n = (r (*) p)(cAddr + (isCN ? ca : oa))
|
||||
#include "il2cpp-unity-functions.h"
|
||||
#undef DO_UNI_FUNC
|
||||
}
|
||||
|
||||
3
lib/src/il2cpp-unity-functions.h
Normal file
3
lib/src/il2cpp-unity-functions.h
Normal file
@@ -0,0 +1,3 @@
|
||||
using namespace Genshin;
|
||||
|
||||
DO_UNI_FUNC(0x00B9D710, 0x00B9D710, ByteArray*, UnityEngine_RecordUserData, (int32_t nType));
|
||||
@@ -13,12 +13,22 @@ string IlStringToString(Il2CppString* str, UINT codePage) {
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region GC
|
||||
|
||||
UINT32 GCHandle_New(void* object, bool pinned) {
|
||||
return il2cpp_gchandle_new((Il2CppObject*)object, pinned);
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region ByteUtils
|
||||
|
||||
bool IsLittleEndian() {
|
||||
UINT i = 1;
|
||||
char* c = (char*)&i;
|
||||
return (*c);
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region FindMainWindowByPID
|
||||
|
||||
@@ -4,11 +4,15 @@ using std::string;
|
||||
|
||||
bool IsLittleEndian();
|
||||
HWND FindMainWindowByPID(DWORD pid);
|
||||
UINT32 GCHandle_New(LPVOID object, bool pinned);
|
||||
string IlStringToString(Il2CppString* str, UINT codePage = CP_ACP);
|
||||
|
||||
#define cstring_new(str) il2cpp_string_new(str)
|
||||
#define string_new(str) cstring_new((str).c_str())
|
||||
|
||||
#define ErrorDialogT(title, msg) MessageBox(unityWnd, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL);
|
||||
#define ErrorDialog(msg) ErrorDialogT("YaeAchievement", msg)
|
||||
#define Win32ErrorDialog(code) ErrorDialogT("YaeAchievement", ("CRITICAL ERROR!\nError code: " + std::to_string(GetLastError()) + "-"#code"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!").c_str())
|
||||
#define Win32ErrorDialog(code) ErrorDialogT("YaeAchievement", ("CRITICAL ERROR!\nError code: " + std::to_string(GetLastError()) + "-"#code"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!").c_str())
|
||||
|
||||
template<class T>
|
||||
static T ReadMapped(void* data, int offset, bool littleEndian = false) {
|
||||
@@ -22,3 +26,8 @@ static T ReadMapped(void* data, int offset, bool littleEndian = false) {
|
||||
memcpy(&result, cData + offset, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static T* GCHandle_GetObject(UINT handle) {
|
||||
return (T*) il2cpp_gchandle_get_target(handle);
|
||||
}
|
||||
|
||||
41
src/AppConfig.cs
Normal file
41
src/AppConfig.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public class AppConfig {
|
||||
|
||||
[JsonProperty(PropertyName = "location")]
|
||||
public string? Location { get; set; }
|
||||
|
||||
private static AppConfig? _instance;
|
||||
|
||||
private static readonly string FileName = Path.Combine(GlobalVars.AppPath, "conf.json");
|
||||
|
||||
public static string GamePath => _instance!.Location!;
|
||||
|
||||
internal static void Load() {
|
||||
if (File.Exists(FileName)) {
|
||||
var text = File.ReadAllText(FileName);
|
||||
_instance = JsonConvert.DeserializeObject<AppConfig>(text)!;
|
||||
}
|
||||
if (_instance?.Location == null || !Utils.CheckGamePathValid(_instance.Location)) {
|
||||
var gameInstallPath = Utils.FindGamePathFromRegistry();
|
||||
if (!string.IsNullOrEmpty(gameInstallPath)) {
|
||||
Console.WriteLine($"自动读取到游戏路径: {gameInstallPath}");
|
||||
Console.WriteLine($"如果确认路径无误,请按 Y ;若有误或需要自行选择,请按 N ");
|
||||
var key = Console.ReadKey().Key;
|
||||
gameInstallPath = key == ConsoleKey.Y ? gameInstallPath : Utils.SelectGameExecutable();
|
||||
} else {
|
||||
gameInstallPath = Utils.SelectGameExecutable();
|
||||
}
|
||||
_instance = new AppConfig {
|
||||
Location = gameInstallPath
|
||||
};
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save() {
|
||||
File.WriteAllText(FileName, JsonConvert.SerializeObject(_instance!, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
39
src/CacheFile.cs
Normal file
39
src/CacheFile.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.IO.Compression;
|
||||
using Google.Protobuf;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
public class CacheFile {
|
||||
|
||||
private readonly string _cacheName;
|
||||
private CacheItem? _content;
|
||||
|
||||
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch;
|
||||
|
||||
public CacheFile(string identifier) {
|
||||
Directory.CreateDirectory(Path.Combine(GlobalVars.AppPath, "cache"));
|
||||
_cacheName = Path.Combine(GlobalVars.AppPath, $"cache/{identifier.MD5Hash()[..16]}.miko");
|
||||
}
|
||||
|
||||
public bool Exists() => File.Exists(_cacheName);
|
||||
|
||||
public CacheItem Read() {
|
||||
if (_content == null) {
|
||||
using var fInput = File.OpenRead(_cacheName);
|
||||
using var dInput = new GZipStream(fInput, CompressionMode.Decompress);
|
||||
_content = CacheItem.Parser.ParseFrom(dInput);
|
||||
}
|
||||
return _content;
|
||||
}
|
||||
|
||||
public void Write(byte[] data, string? etag = null) {
|
||||
using var fOut = File.OpenWrite(_cacheName);
|
||||
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize);
|
||||
new CacheItem {
|
||||
Etag = etag ?? string.Empty,
|
||||
Version = 3,
|
||||
Checksum = data.MD5Hash(),
|
||||
Content = ByteString.CopyFrom(data)
|
||||
}.WriteTo(cOut);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.Win32;
|
||||
@@ -10,19 +10,25 @@ namespace YaeAchievement;
|
||||
public static class Export {
|
||||
|
||||
public static void Choose(AchievementAllDataNotify data) {
|
||||
Console.Write(@"导出至:
|
||||
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
|
||||
[1] SnapGenshin
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] 表格文件
|
||||
输入一个数字(0-4): ".Split("\n").Select(s => s.Trim()).JoinToString("\n") + " ");
|
||||
Console.Write("""
|
||||
导出至:
|
||||
[0] 椰羊 (https://cocogoat.work/achievement, 默认)
|
||||
[1] SnapGenshin
|
||||
[2] Paimon.moe
|
||||
[3] Seelie.me
|
||||
[4] 表格文件
|
||||
[5] 原魔工具箱
|
||||
[6] 寻空
|
||||
输入一个数字(0-6):
|
||||
""");
|
||||
if (!int.TryParse(Console.ReadLine(), out var num)) num = 0;
|
||||
((Action<AchievementAllDataNotify>) (num switch {
|
||||
1 => ToSnapGenshin,
|
||||
2 => ToPaimon,
|
||||
3 => ToSeelie,
|
||||
4 => ToCSV,
|
||||
5 => ToWxApp1,
|
||||
6 => ToXunkong,
|
||||
7 => ToRawJson,
|
||||
_ => ToCocogoat
|
||||
})).Invoke(data);
|
||||
@@ -46,6 +52,21 @@ public static class Export {
|
||||
: $"https://cocogoat.work/achievement?memo={memo.key}");
|
||||
}
|
||||
|
||||
private static void ToWxApp1(AchievementAllDataNotify data) {
|
||||
var id = Guid.NewGuid().ToString("N").Substring(20, 8);
|
||||
var result = JsonConvert.SerializeObject(new Dictionary<string, object> {
|
||||
{ "key", id },
|
||||
{ "data", ExportToUIAFApp(data) }
|
||||
});
|
||||
using var request = new HttpRequestMessage {
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri("https://api.qyinter.com/achievementRedis"),
|
||||
Content = new StringContent(result, Encoding.UTF8, "application/json")
|
||||
};
|
||||
using var response = Utils.CHttpClient.Value.Send(request);
|
||||
Console.WriteLine($"在小程序导入页面输入以下代码: {id}");
|
||||
}
|
||||
|
||||
private static void ToSnapGenshin(AchievementAllDataNotify data) {
|
||||
if (CheckSnapScheme()) {
|
||||
Utils.CopyToClipboard(JsonConvert.SerializeObject(ExportToUIAFApp(data)));
|
||||
@@ -122,6 +143,17 @@ public static class Export {
|
||||
Console.WriteLine($"成就数据已导出至 {path}");
|
||||
}
|
||||
|
||||
private static void ToXunkong(AchievementAllDataNotify data) {
|
||||
if (CheckXunkongScheme()) {
|
||||
Utils.CopyToClipboard(JsonConvert.SerializeObject(ExportToUIAFApp(data)));
|
||||
Utils.ShellOpen("xunkong://import-achievement?caller=YaeAchievement&from=clipboard");
|
||||
Console.WriteLine("在寻空中进行下一步操作");
|
||||
} else {
|
||||
Console.WriteLine("更新寻空至最新版本后重试");
|
||||
Utils.ShellOpen("ms-windows-store://pdp/?productid=9N2SVG0JMT12");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToRawJson(AchievementAllDataNotify data) {
|
||||
var path = Path.GetFullPath($"export-{DateTime.Now:yyyyMMddHHmmss}-raw.json");
|
||||
File.WriteAllText(path, JsonConvert.SerializeObject(data, Formatting.Indented));
|
||||
@@ -131,25 +163,35 @@ public static class Export {
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static Dictionary<string, object> ExportToUIAFApp(AchievementAllDataNotify data) {
|
||||
var output = data.List
|
||||
.Where(a => a.Status is Status.Finished or Status.RewardTaken)
|
||||
.Select(ach => new Dictionary<string, uint> { ["id"] = ach.Id, ["current"] = ach.Current, ["timestamp"] = ach.Timestamp })
|
||||
.Where(a => (uint)a.Status > 1 || a.Current > 0)
|
||||
.Select(ach => new Dictionary<string, uint> {
|
||||
["id"] = ach.Id,
|
||||
["status"] = (uint) ach.Status,
|
||||
["current"] = ach.Current,
|
||||
["timestamp"] = ach.Timestamp,
|
||||
})
|
||||
.ToList();
|
||||
return new Dictionary<string, object> {
|
||||
["info"] = new Dictionary<string, object> {
|
||||
["export_app"] = "YaeAchievement",
|
||||
["export_timestamp"] = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
|
||||
["export_app_version"] = GlobalVars.AppVersionName,
|
||||
["uiaf_version"] = "v1.0"
|
||||
["uiaf_version"] = "v1.1"
|
||||
},
|
||||
["list"] = output
|
||||
};
|
||||
}
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1416:验证平台兼容性")]
|
||||
#pragma warning disable CA1416
|
||||
private static bool CheckSnapScheme() {
|
||||
return (string?)Registry.ClassesRoot.OpenSubKey("snapgenshin")?.GetValue("") == "URL:snapgenshin";
|
||||
}
|
||||
|
||||
|
||||
private static bool CheckXunkongScheme() {
|
||||
return (string?)Registry.ClassesRoot.OpenSubKey("xunkong")?.GetValue("") == "URL:xunkong";
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
|
||||
private static string JoinToString(this IEnumerable<object> list, string separator) {
|
||||
return string.Join(separator, list);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public static class Extensions {
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<Aes> aes = new (Aes.Create);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<MD5> md5 = new (MD5.Create);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly Lazy<HttpClient> defaultClient = new (() => new HttpClient(new HttpClientHandler {
|
||||
Proxy = GlobalVars.DebugProxy ? new WebProxy("http://127.0.0.1:8888") : null
|
||||
}) {
|
||||
DefaultRequestHeaders = {{ "User-Agent", "UnityPlayer/2017.4.30f1 (UnityWebRequest/1.0, libcurl/7.51.0-DEV)" }}
|
||||
});
|
||||
|
||||
|
||||
public static byte[] ToBytes(this string text) {
|
||||
return Encoding.UTF8.GetBytes(text);
|
||||
}
|
||||
|
||||
public static string DecodeToString(this byte[] bytes) {
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this string text) {
|
||||
return text.ToBytes().MD5Hash();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static string MD5Hash(this string text) {
|
||||
return md5.Value.ComputeHash(text.ToBytes()).ToHex();
|
||||
public static string MD5Hash(this byte[] data) {
|
||||
return md5.Value.ComputeHash(data).ToHex().ToLower();
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
@@ -43,55 +31,6 @@ public static class Extensions {
|
||||
return base64 ? bytes.ToBase64() : bytes.ToHex();
|
||||
}
|
||||
|
||||
public static HttpResponseMessage Send(this HttpRequestMessage message, HttpClient? client = null) {
|
||||
Logger.Trace($"{message.Method} {message.RequestUri?.GetFullPath()}");
|
||||
return (client ?? defaultClient.Value).Send(message); // dispose message?
|
||||
}
|
||||
|
||||
public static T? Send<T>(this HttpRequestMessage message, HttpClient? client = null, Func<string, string>? onPreDeserialize = null) {
|
||||
Logger.Trace($"{message.Method} {message.RequestUri?.GetFullPath()}");
|
||||
using var response = (client ?? defaultClient.Value).Send(message);
|
||||
var text = response.Content.ReadAsStringAsync().Result;
|
||||
if (onPreDeserialize != null) {
|
||||
text = onPreDeserialize.Invoke(text);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(text);
|
||||
}
|
||||
|
||||
public static string ToQueryString(this NameValueCollection collection, bool escape = true) {
|
||||
var items = collection.AllKeys
|
||||
.Select(key => escape ?
|
||||
$"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(collection[key])}" :
|
||||
$"{key}={collection[key]}");
|
||||
return string.Join("&", items);
|
||||
}
|
||||
|
||||
public static string ToQueryString(this IEnumerable<KeyValuePair<string, object>> dict, bool escape = true) {
|
||||
var items = dict
|
||||
.Select(pair => escape ?
|
||||
$"{HttpUtility.UrlEncode(pair.Key)}={HttpUtility.UrlEncode(pair.Value.ToString())}" :
|
||||
$"{pair.Key}={pair.Value}");
|
||||
return string.Join("&", items);
|
||||
}
|
||||
|
||||
public static string ToJsonString<TKey, TValue>(this Dictionary<TKey, TValue> data) where TKey : notnull {
|
||||
return JsonConvert.SerializeObject(data);
|
||||
}
|
||||
|
||||
public static string GetFullPath(this Uri uri) {
|
||||
return $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}";
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<string, string>> ToKeyValuePairs(this NameValueCollection collection) {
|
||||
return collection.AllKeys
|
||||
.Select(key => new KeyValuePair<string, string>(key!, collection[key] ?? string.Empty))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static string UrlEncode(this string text) {
|
||||
return HttpUtility.UrlEncode(text);
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] bytes) {
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
@@ -99,8 +38,4 @@ public static class Extensions {
|
||||
public static string ToBase64(this byte[] bytes) {
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
public static byte[] FromBase64(this string text) {
|
||||
return Convert.FromBase64String(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertToConstant.Global")]
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global")]
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable ConvertToConstant.Global
|
||||
// ReSharper disable FieldCanBeMadeReadOnly.Global
|
||||
#pragma warning disable CA2211
|
||||
|
||||
public static class GlobalVars {
|
||||
|
||||
public static bool DebugProxy = false;
|
||||
public static bool CheckGamePath = true;
|
||||
public static bool UnexpectedExit = true;
|
||||
public static string GamePath = null!;
|
||||
public static Logger.Level LogLevel = Logger.Level.Info;
|
||||
public static Version AppVersion = Assembly.GetEntryAssembly()!.GetName().Version!;
|
||||
public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
public const uint AppVersionCode = 28;
|
||||
public const string AppVersionName = "2.0";
|
||||
public const uint AppVersionCode = 29;
|
||||
public const string AppVersionName = "2.1";
|
||||
public const string LibName = "YaeLib.dll";
|
||||
public const string PipeName = "YaeAchievementPipe";
|
||||
public const string BucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
|
||||
public const string ConfigFileName = "YaeAchievement.runtimeconfig.json";
|
||||
|
||||
}
|
||||
#pragma warning restore CA2211
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using YaeAchievement.Win32;
|
||||
using static YaeAchievement.Win32.Native;
|
||||
|
||||
namespace YaeAchievement;
|
||||
|
||||
@@ -19,31 +20,33 @@ public static class Injector {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int LoadLibraryAndInject(IntPtr handle, string libPath) {
|
||||
var hKernel = Native.GetModuleHandle("kernel32.dll");
|
||||
// todo: refactor
|
||||
public static int LoadLibraryAndInject(IntPtr hProc, string libPath) {
|
||||
var hKernel = GetModuleHandle("kernel32.dll");
|
||||
if (hKernel == IntPtr.Zero) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetModuleHandle fail");
|
||||
}
|
||||
var pLoadLibrary = Native.GetProcAddress(hKernel, "LoadLibraryA");
|
||||
var pLoadLibrary = GetProcAddress(hKernel, "LoadLibraryA");
|
||||
if (pLoadLibrary == IntPtr.Zero) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("GetProcAddress fail");
|
||||
}
|
||||
var pBase = Native.VirtualAllocEx(handle, IntPtr.Zero, libPath.Length + 1, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ReadWrite);
|
||||
var pBase = VirtualAllocEx(hProc, IntPtr.Zero, libPath.Length + 1, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ReadWrite);
|
||||
if (pBase == IntPtr.Zero) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("VirtualAllocEx fail");
|
||||
}
|
||||
if (!Native.WriteProcessMemory(handle, pBase, libPath.ToCharArray(), libPath.Length, out _)) {
|
||||
if (!WriteProcessMemory(hProc, pBase, libPath.ToCharArray(), libPath.Length, out _)) {
|
||||
return new Win32Exception().PrintMsgAndReturnErrCode("WriteProcessMemory fail");
|
||||
}
|
||||
var hThread = Native.CreateRemoteThread(handle, IntPtr.Zero, 0, pLoadLibrary, pBase, 0, out _);
|
||||
var hThread = CreateRemoteThread(hProc, IntPtr.Zero, 0, pLoadLibrary, pBase, 0, out _);
|
||||
if (hThread == IntPtr.Zero) {
|
||||
var e = new Win32Exception();
|
||||
if (!Native.VirtualFreeEx(handle, pBase, 0, AllocationType.Release)) {
|
||||
new Win32Exception().PrintMsgAndReturnErrCode("VirtualFreeEx fail");
|
||||
}
|
||||
VirtualFreeEx(hProc, pBase, 0, AllocationType.Release);
|
||||
return e.PrintMsgAndReturnErrCode("CreateRemoteThread fail");
|
||||
}
|
||||
return !Native.CloseHandle(hThread) ? new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail") : 0;
|
||||
if (WaitForSingleObject(hThread, 2000) == 0) {
|
||||
VirtualFreeEx(hProc, pBase, 0, AllocationType.Release);
|
||||
}
|
||||
return !CloseHandle(hThread) ? new Win32Exception().PrintMsgAndReturnErrCode("CloseHandle fail") : 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
namespace YaeAchievement;
|
||||
|
||||
public static class Logger {
|
||||
|
||||
public enum Level {
|
||||
Trace, Debug, Info, Warn, Error
|
||||
}
|
||||
|
||||
public static void Error(string msg) {
|
||||
Log(msg, Level.Error);
|
||||
}
|
||||
|
||||
public static void Warn(string msg) {
|
||||
Log(msg, Level.Warn);
|
||||
}
|
||||
|
||||
public static void Info(string msg) {
|
||||
Log(msg, Level.Info);
|
||||
}
|
||||
|
||||
public static void Debug(string msg) {
|
||||
Log(msg, Level.Debug);
|
||||
}
|
||||
|
||||
public static void Trace(string msg) {
|
||||
Log(msg, Level.Trace);
|
||||
}
|
||||
|
||||
private static void Log(string msg, Level level) {
|
||||
if (level >= GlobalVars.LogLevel) {
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLog(string msg, Level level = Level.Info) {
|
||||
if (level >= GlobalVars.LogLevel) {
|
||||
Console.Write($"{DateTime.Now:MM/dd HH:mm:ss} {level.ToString().ToUpper().PadLeft(5)} : {msg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ using YaeAchievement.AppCenterSDK.Models;
|
||||
using static YaeAchievement.Utils;
|
||||
|
||||
InstallExitHook();
|
||||
|
||||
CheckVcRuntime();
|
||||
CheckIsTempDir();
|
||||
CheckSelfIsRunning();
|
||||
TryDisableQuickEdit();
|
||||
InstallExceptionHook();
|
||||
@@ -14,7 +17,7 @@ Console.WriteLine($"YaeAchievement - 原神成就导出工具 ({GlobalVars.AppVe
|
||||
Console.WriteLine("https://github.com/HolographicHat/YaeAchievement");
|
||||
Console.WriteLine("----------------------------------------------------");
|
||||
|
||||
LoadConfig();
|
||||
AppConfig.Load();
|
||||
CheckUpdate();
|
||||
AppCenter.Init();
|
||||
new EventLog("AppInit") {
|
||||
@@ -23,9 +26,18 @@ new EventLog("AppInit") {
|
||||
{ "SystemVersion", DeviceHelper.GetSystemVersion() }
|
||||
}
|
||||
}.Enqueue();
|
||||
StartAndWaitResult(GlobalVars.GamePath, str => {
|
||||
GlobalVars.UnexpectedExit = false;
|
||||
var list = AchievementAllDataNotify.Parser.ParseFrom(Convert.FromBase64String(str));
|
||||
Export.Choose(list);
|
||||
return true;
|
||||
});
|
||||
var historyCache = new CacheFile("ExportData");
|
||||
if (historyCache.LastWriteTime.AddMinutes(10) > DateTime.UtcNow) {
|
||||
Console.WriteLine("使用上一次获取到的成就数据");
|
||||
Console.WriteLine("要重新获取数据,手动删除 cache\\d1a8ef40a67a5929.miko 后重新启动 YaeAchievement");
|
||||
Export.Choose(AchievementAllDataNotify.Parser.ParseFrom(historyCache.Read().Content));
|
||||
} else {
|
||||
StartAndWaitResult(AppConfig.GamePath, str => {
|
||||
GlobalVars.UnexpectedExit = false;
|
||||
var data = Convert.FromBase64String(str);
|
||||
var list = AchievementAllDataNotify.Parser.ParseFrom(data);
|
||||
historyCache.Write(data);
|
||||
Export.Choose(list);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
139
src/Utils.cs
139
src/Utils.cs
@@ -1,14 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Pipes;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.Win32;
|
||||
using YaeAchievement.AppCenterSDK;
|
||||
using YaeAchievement.Win32;
|
||||
using static YaeAchievement.Win32.OpenFileFlags;
|
||||
@@ -31,58 +27,28 @@ public static class Utils {
|
||||
return c;
|
||||
});
|
||||
|
||||
public static string GetBucketFileAsString(string path, bool cache = true) {
|
||||
return Encoding.UTF8.GetString(GetBucketFileAsByteArray(path, cache));
|
||||
}
|
||||
|
||||
public static byte[] GetBucketFileAsByteArray(string path, bool cache = true) {
|
||||
using var msg = new HttpRequestMessage {
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri($"{GlobalVars.BucketHost}/{path}")
|
||||
};
|
||||
CacheItem? ci = null;
|
||||
var cacheName = cache ? $"./cache/{CalculateMD5(path)[..16]}.miko" : "";
|
||||
if (cache) {
|
||||
Directory.CreateDirectory("cache");
|
||||
if (File.Exists(cacheName)) {
|
||||
using var input = File.OpenRead(cacheName);
|
||||
using var dInput = new GZipStream(input, CompressionMode.Decompress);
|
||||
ci = CacheItem.Parser.ParseFrom(dInput);
|
||||
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{ci.Etag}");
|
||||
}
|
||||
var cacheFile = new CacheFile(path);
|
||||
if (cache && cacheFile.Exists()) {
|
||||
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}");
|
||||
}
|
||||
using var response = CHttpClient.Value.Send(msg);
|
||||
if (cache && response.StatusCode == HttpStatusCode.NotModified) {
|
||||
return ci!.Content.ToByteArray();
|
||||
return cacheFile.Read().Content.ToByteArray();
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
var responseBytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||
if (cache) {
|
||||
var etag = response.Headers.ETag!.Tag;
|
||||
using var os = File.OpenWrite(cacheName);
|
||||
using var cos = new GZipStream(os, CompressionLevel.SmallestSize);
|
||||
new CacheItem {
|
||||
Etag = etag,
|
||||
Version = 3,
|
||||
Checksum = CalculateMD5(responseBytes),
|
||||
Content = ByteString.CopyFrom(responseBytes)
|
||||
}.WriteTo(cos);
|
||||
cacheFile.Write(responseBytes, etag);
|
||||
}
|
||||
return responseBytes;
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static string CalculateMD5(string text) {
|
||||
return CalculateMD5(Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static string CalculateMD5(byte[] bytes) {
|
||||
using var md5 = MD5.Create();
|
||||
var b = md5.ComputeHash(bytes);
|
||||
return Convert.ToHexString(b).ToLower();
|
||||
}
|
||||
|
||||
public static void CopyToClipboard(string text) {
|
||||
if (Native.OpenClipboard(IntPtr.Zero)) {
|
||||
Native.EmptyClipboard();
|
||||
@@ -98,18 +64,6 @@ public static class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadConfig() {
|
||||
var conf = JsonNode.Parse(File.ReadAllText(GlobalVars.ConfigFileName))!;
|
||||
var path = conf["location"];
|
||||
if (path == null || !CheckGamePathValid(path.GetValue<string>())) {
|
||||
GlobalVars.GamePath = SelectGameExecutable();
|
||||
conf["location"] = GlobalVars.GamePath;
|
||||
File.WriteAllText(GlobalVars.ConfigFileName, conf.ToJsonString());
|
||||
} else {
|
||||
GlobalVars.GamePath = path.GetValue<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void CheckUpdate() {
|
||||
var info = UpdateInfo.Parser.ParseFrom(GetBucketFileAsByteArray("schicksal/version"))!;
|
||||
if (GlobalVars.AppVersionCode != info.VersionCode) {
|
||||
@@ -125,7 +79,6 @@ public static class Utils {
|
||||
}
|
||||
Console.WriteLine($"下载地址: {info.PackageLink}");
|
||||
if (info.ForceUpdate) {
|
||||
//Console.WriteLine("在完成此次更新前, 程序可能无法正常使用.");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
@@ -139,13 +92,20 @@ public static class Utils {
|
||||
var cur = Process.GetCurrentProcess();
|
||||
foreach (var process in Process.GetProcesses().Where(process => process.Id != cur.Id)) {
|
||||
if (process.ProcessName == cur.ProcessName) {
|
||||
Logger.Error("另一个实例正在运行,请关闭后重试");
|
||||
Console.WriteLine("另一个实例正在运行,请关闭后重试");
|
||||
Environment.Exit(302);
|
||||
}
|
||||
}
|
||||
Process.LeaveDebugMode();
|
||||
}
|
||||
|
||||
public static void CheckIsTempDir() {
|
||||
if (GlobalVars.AppPath.Contains(Path.GetTempPath())) {
|
||||
Console.WriteLine("请将程序完整解压后再运行");
|
||||
Environment.Exit(303);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShellOpen(string path) {
|
||||
return new Process {
|
||||
StartInfo = {
|
||||
@@ -155,13 +115,13 @@ public static class Utils {
|
||||
}.Start();
|
||||
}
|
||||
|
||||
private static bool CheckGamePathValid(string path) {
|
||||
public static bool CheckGamePathValid(string? path) {
|
||||
if (path == null) return false;
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
return !GlobalVars.CheckGamePath ||
|
||||
File.Exists($"{dir}/UnityPlayer.dll") && File.Exists($"{dir}/mhypbase.dll");
|
||||
return !GlobalVars.CheckGamePath || File.Exists(Path.Combine(dir, "UnityPlayer.dll"));
|
||||
}
|
||||
|
||||
private static string SelectGameExecutable() {
|
||||
public static string SelectGameExecutable() {
|
||||
var fnPtr = Marshal.AllocHGlobal(32768);
|
||||
Native.RtlZeroMemory(fnPtr, 32768);
|
||||
var ofn = new OpenFileName {
|
||||
@@ -204,12 +164,12 @@ public static class Utils {
|
||||
var handle = Native.GetStdHandle();
|
||||
return Native.GetConsoleMode(handle, out var mode) && Native.SetConsoleMode(handle, mode&~64);
|
||||
}
|
||||
|
||||
|
||||
public static void CheckGenshinIsRunning() {
|
||||
Process.EnterDebugMode();
|
||||
foreach (var process in Process.GetProcesses()) {
|
||||
if (process.ProcessName is "GenshinImpact" or "YuanShen") {
|
||||
Console.WriteLine("原神正在运行,请关闭后重试");
|
||||
if (process.ProcessName is "GenshinImpact" or "YuanShen" && !process.HasExited) {
|
||||
Console.WriteLine($"原神正在运行,请关闭后重试 ({process.Id})");
|
||||
Environment.Exit(301);
|
||||
}
|
||||
}
|
||||
@@ -222,7 +182,7 @@ public static class Utils {
|
||||
public static void InstallExitHook() {
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
|
||||
proc?.Kill();
|
||||
Logger.Info("按任意键退出");
|
||||
Console.WriteLine("按任意键退出");
|
||||
Console.ReadKey();
|
||||
};
|
||||
}
|
||||
@@ -230,7 +190,7 @@ public static class Utils {
|
||||
public static void InstallExceptionHook() {
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
|
||||
Console.WriteLine(e.ExceptionObject.ToString());
|
||||
Logger.Error("正在上报错误信息...");
|
||||
Console.WriteLine("正在上报错误信息...");
|
||||
AppCenter.TrackCrash((Exception) e.ExceptionObject);
|
||||
AppCenter.Upload();
|
||||
Environment.Exit(-1);
|
||||
@@ -241,6 +201,9 @@ public static class Utils {
|
||||
public static Thread StartAndWaitResult(string exePath, Func<string, bool> onReceive) {
|
||||
const string lib = "C:/ProgramData/yae.dll";
|
||||
File.Copy(Path.GetFullPath(GlobalVars.LibName), lib, true);
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
|
||||
File.Delete(lib);
|
||||
};
|
||||
if (!Injector.CreateProcess(exePath, out var hProcess, out var hThread, out var pid)) {
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("ICreateProcess fail"));
|
||||
}
|
||||
@@ -249,7 +212,7 @@ public static class Utils {
|
||||
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail"));
|
||||
}
|
||||
}
|
||||
Logger.Info($"原神正在启动 ({pid})");
|
||||
Console.WriteLine($"原神正在启动 ({pid})");
|
||||
proc = Process.GetProcessById(Convert.ToInt32(pid));
|
||||
proc.EnableRaisingEvents = true;
|
||||
proc.Exited += (_, _) => {
|
||||
@@ -259,9 +222,6 @@ public static class Utils {
|
||||
Environment.Exit(114514);
|
||||
}
|
||||
};
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
|
||||
File.Delete(lib);
|
||||
};
|
||||
if (Native.ResumeThread(hThread) == 0xFFFFFFFF) {
|
||||
var e = new Win32Exception();
|
||||
if (!Native.TerminateProcess(hProcess, 0)) {
|
||||
@@ -291,4 +251,47 @@ public static class Utils {
|
||||
th.Start();
|
||||
return th;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416
|
||||
/// <summary>
|
||||
/// 从注册表中寻找安装路径 暂时只支持国服
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string? FindGamePathFromRegistry() {
|
||||
try {
|
||||
using var root = Registry.LocalMachine;
|
||||
using var sub = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神");
|
||||
if (sub == null) {
|
||||
return null;
|
||||
}
|
||||
var installLocation = sub.GetValue("InstallPath")?.ToString();
|
||||
if (!string.IsNullOrEmpty(installLocation)) {
|
||||
var folder = Path.Combine(installLocation, "Genshin Impact Game\\");
|
||||
var exePath = Path.Combine(folder, "YuanShen.exe");
|
||||
if (File.Exists(Path.Combine(folder, "UnityPlayer.dll")) && File.Exists(exePath)) {
|
||||
return exePath;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void CheckVcRuntime() {
|
||||
using var root = Registry.LocalMachine;
|
||||
using var sub = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")!;
|
||||
var installed = sub.GetSubKeyNames()
|
||||
.Select(subKeyName => sub.OpenSubKey(subKeyName))
|
||||
.Select(item => item?.GetValue("DisplayName") as string ?? string.Empty)
|
||||
.Any(name => name.Contains("Microsoft Visual C++ 2022 X64 "));
|
||||
if (!installed) {
|
||||
const string vcDownloadUrl = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
|
||||
Console.WriteLine("未安装 VcRuntime");
|
||||
Console.WriteLine($"下载地址: {vcDownloadUrl}");
|
||||
Console.WriteLine("安装完成后,重新打开 YaeAchievement");
|
||||
ShellOpen(vcDownloadUrl);
|
||||
Environment.Exit(303);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
[Flags]
|
||||
public enum AllocationType : uint {
|
||||
Commit = 0x1000,
|
||||
Reserve = 0x2000,
|
||||
Decommit = 0x4000,
|
||||
Release = 0x8000,
|
||||
Reset = 0x80000,
|
||||
Physical = 0x400000,
|
||||
TopDown = 0x100000,
|
||||
WriteWatch = 0x200000,
|
||||
LargePages = 0x20000000
|
||||
Commit = 0x00001000,
|
||||
Reserve = 0x00002000,
|
||||
Reset = 0x00080000,
|
||||
TopDown = 0x00100000,
|
||||
WriteWatch = 0x00200000,
|
||||
Physical = 0x00400000,
|
||||
Rotate = 0x00800000,
|
||||
ResetUndo = 0x01000000,
|
||||
LargePages = 0x20000000,
|
||||
Decommit = 0x00004000,
|
||||
Release = 0x00008000
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace YaeAchievement.Win32;
|
||||
public static class Extensions {
|
||||
|
||||
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
|
||||
Logger.Error($"{msg}: {ex.Message}");
|
||||
Console.WriteLine($"{msg}: {ex.Message}");
|
||||
AppCenter.TrackCrash(ex, false);
|
||||
return ex.NativeErrorCode;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace YaeAchievement.Win32;
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1401:P/Invokes 应该是不可见的")]
|
||||
#pragma warning disable CA1401, CA2101
|
||||
public static class Native {
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
|
||||
public static extern bool CreateProcess(
|
||||
string lpApplicationName,
|
||||
string? lpCommandLine,
|
||||
@@ -38,7 +38,6 @@ public static class Native {
|
||||
public static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
[SuppressMessage("Globalization", "CA2101:指定对 P/Invoke 字符串参数进行封送处理")]
|
||||
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
|
||||
@@ -147,4 +146,7 @@ public static class Native {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hdc);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern uint WaitForSingleObject(IntPtr handle, ulong dwMilliseconds);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user