22 Commits
2.0.0 ... 2.1.0

Author SHA1 Message Date
HolographicHat
4e94d67d0b Bump version to 2.1 2022-08-24 17:03:06 +08:00
HolographicHat
7dafd95099 Merge remote-tracking branch 'origin/master' 2022-08-24 17:02:53 +08:00
HolographicHat
76c20407ea Update README.md 2022-08-23 00:00:33 +08:00
HolographicHat
b79b82ec10 Merge pull request #24 from xunkong/hat
Add Xunkong and adapt to UIAF 1.1
2022-08-22 23:59:59 +08:00
Scighost
07fe60a092 Add Xunkong and adapt to UIAF 1.1 2022-08-22 23:37:27 +08:00
HolographicHat
7fa2fbac25 3.0 offsets 2022-08-22 14:25:35 +08:00
HolographicHat
28ffa6fb1a UIAF 1.1 2022-08-21 12:10:47 +08:00
HolographicHat
4c2cb28313 add export 2022-08-21 12:06:50 +08:00
HolographicHat
2f1a5ad99e #23 2022-08-19 18:09:49 +08:00
HolographicHat
9b0c384d4b Add AppConfig 2022-08-19 18:09:30 +08:00
HolographicHat
b2111db4eb Implement #22 2022-08-15 23:36:23 +08:00
HolographicHat
2442264224 Add CacheFile 2022-08-15 23:35:29 +08:00
HolographicHat
b596cad02e Remove unused code 2022-08-15 23:34:47 +08:00
HolographicHat
30a0189f5e Merge remote-tracking branch 'origin/master' 2022-08-14 21:42:38 +08:00
HolographicHat
f47fd234b4 add vcruntime check 2022-08-14 21:42:21 +08:00
HolographicHat
7d306a60c9 Update README.md 2022-08-14 21:28:30 +08:00
HolographicHat
10dd03335f lib update 2022-08-12 23:34:04 +08:00
HolographicHat
41863c32f7 optimize 2022-07-16 01:41:11 +08:00
HolographicHat
4c3e9d8e50 Fix os2.8 crash 2022-07-15 23:02:00 +08:00
HolographicHat
9d60cda4c7 Merge pull request #19 from huiyadanli/master
Add feat: get game path from registry (YuanShen.exe)
2022-07-15 22:03:55 +08:00
huiyadanli
e0c836e55d Add feat: get game path from registry (YuanShen.exe) 2022-07-15 21:51:36 +08:00
HolographicHat
02b4d9df0b Update README.md 2022-07-15 16:34:36 +08:00
23 changed files with 381 additions and 249 deletions

7
.gitignore vendored
View File

@@ -4,3 +4,10 @@ obj/
riderModule.iml
/_ReSharper.Caches/
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
.vs/

View File

@@ -2,6 +2,29 @@
# YaeAchievement
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/genshin-achievement-export?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/genshin-achievement-export?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/genshin-achievement-export/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/YaeAchievement?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/YaeAchievement?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/YaeAchievement/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
</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)所在路径
![alt](https://upload-bbs.mihoyo.com/upload/2022/04/06/165631158/e540a5a6d50cd5fdee19665435548e00_514247033566841954.jpg)
设置完毕后,等待原神自动启动并退出
## 下载地址
[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: 不要把软件和原神主程序放一起

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
using namespace Genshin;
DO_UNI_FUNC(0x00B9D710, 0x00B9D710, ByteArray*, UnityEngine_RecordUserData, (int32_t nType));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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