12 Commits
4.1.0 ... 5.2.2

Author SHA1 Message Date
HolographicHat
1b861712eb fix #119 #120 2024-11-21 21:35:41 +08:00
HolographicHat
fb4e3f8d00 fix 2024-11-21 08:11:26 +08:00
HolographicHat
b1135542c1 5.2 2024-11-21 07:33:07 +08:00
HolographicHat
7d3d0f5e14 auto identify field ids 2024-11-21 07:31:21 +08:00
HolographicHat
4268b04f3c ignore zydis 2024-10-15 20:41:04 +08:00
HolographicHat
9a9d1310a1 Merge pull request #115 from 34736384/master 2024-10-15 20:37:59 +08:00
REL
618a9189ad use sse 2024-10-15 07:32:14 -04:00
REL
f73dbdc4fe switch to amalgamated zydis 2024-10-14 20:24:32 -04:00
REL
298134c063 added dynamic resolving of CmdId and ToUInt16 2024-10-14 05:54:32 -04:00
REL
e9ace26d69 refactor 2024-10-10 05:06:57 -04:00
REL
2c15353f86 remove detours 2024-10-09 08:32:36 -04:00
REL
99fec63867 remove il2cpp bloat 2024-10-09 08:14:41 -04:00
32 changed files with 68162 additions and 1713 deletions

2
.gitattributes vendored
View File

@@ -1,4 +1,4 @@
lib/src/il2cpp-types.h linguist-generated=true
lib/src/Zydis.* linguist-generated=true
# Auto detect text files and perform LF normalization
* text=auto

View File

@@ -20,9 +20,9 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
<UseDebugLibraries>true</UseDebugLibraries>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -30,7 +30,6 @@
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<EnableASAN>false</EnableASAN>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
@@ -45,34 +44,28 @@
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
<TargetName>YaeLib</TargetName>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>build\$(Platform)\$(Configuration)\</IntDir>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_DEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>NotSet</SubSystem>
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net6.0</Command>
@@ -83,48 +76,36 @@
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
<DebugInformationFormat>None</DebugInformationFormat>
<PreprocessorDefinitions>_AMD64_;NDEBUG;YAEACHIEVEMENTLIB_EXPORTS;_WINDOWS;_USRDLL;WIN32_LEAN_AND_MEAN;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<AdditionalIncludeDirectories>$(ProjectDir)lib\detours\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<SDLCheck>true</SDLCheck>
<LanguageStandard_C>stdc11</LanguageStandard_C>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<SubSystem>NotSet</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>$(ProjectDir)lib\detours\;$(OutDir)</AdditionalLibraryDirectories>
<AdditionalDependencies>detours-x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net8.0-windows\win-x64\YaeAchievementLib.dll /y</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="src\HookManager.h" />
<ClInclude Include="src\il2cpp-appdata.h" />
<ClInclude Include="src\il2cpp-functions.h" />
<ClInclude Include="src\globals.h" />
<ClInclude Include="src\il2cpp-types.h" />
<ClInclude Include="src\il2cpp-init.h" />
<ClInclude Include="src\pch.h" />
<ClInclude Include="src\util.h" />
<ClInclude Include="src\Zydis.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\dllmain.cpp" />
<ClCompile Include="src\il2cpp-init.cpp" />
<ClCompile Include="src\pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="src\util.cpp" />
<ClCompile Include="src\Zydis.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
//////////////////////////////////////////////////////////////////////////////
//
// Common version parameters.
//
// Microsoft Research Detours Package, Version 4.0.1
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
#define _USING_V110_SDK71_ 1
#include "winver.h"
#if 0
#include <windows.h>
#include <detours.h>
#else
#ifndef DETOURS_STRINGIFY
#define DETOURS_STRINGIFY_(x) #x
#define DETOURS_STRINGIFY(x) DETOURS_STRINGIFY_(x)
#endif
#define VER_FILEFLAGSMASK 0x3fL
#define VER_FILEFLAGS 0x0L
#define VER_FILEOS 0x00040004L
#define VER_FILETYPE 0x00000002L
#define VER_FILESUBTYPE 0x00000000L
#endif
#define VER_DETOURS_BITS DETOURS_STRINGIFY(DETOURS_BITS)

View File

@@ -1,66 +0,0 @@
#pragma once
#include "pch.h"
#define CALL_ORIGIN(function, ...) \
HookManager::call(function, __func__, __VA_ARGS__)
class HookManager {
public:
template <typename Fn>
static void install(Fn func, Fn handler) {
enable(func, handler);
holderMap[reinterpret_cast<void*>(handler)] = reinterpret_cast<void*>(func);
}
template <typename Fn>
[[nodiscard]] static Fn getOrigin(Fn handler, const char* callerName = nullptr) noexcept {
if (holderMap.count(reinterpret_cast<void*>(handler)) == 0) {
printf("Origin not found for handler: %s. Maybe racing bug.", callerName == nullptr ? "<Unknown>" : callerName);
return nullptr;
}
return reinterpret_cast<Fn>(holderMap[reinterpret_cast<void*>(handler)]);
}
template <typename Fn>
[[nodiscard]] static void detach(Fn handler) noexcept {
disable(handler);
holderMap.erase(reinterpret_cast<void*>(handler));
}
template <typename RType, typename... Params>
[[nodiscard]] static RType call(RType(*handler)(Params...), const char* callerName = nullptr, Params... params) {
auto origin = getOrigin(handler, callerName);
if (origin != nullptr)
return origin(params...);
return RType();
}
static void detachAll() noexcept {
for (const auto &[key, value] : holderMap) {
disable(key);
}
holderMap.clear();
}
private:
inline static std::map<void*, void*> holderMap{};
template <typename Fn>
static void disable(Fn handler) {
Fn origin = getOrigin(handler);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)origin, handler);
DetourTransactionCommit();
}
template <typename Fn>
static void enable(Fn& func, Fn handler) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)func, handler);
DetourTransactionCommit();
}
};

54990
lib/src/Zydis.c Normal file

File diff suppressed because one or more lines are too long

12113
lib/src/Zydis.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,186 @@
// ReSharper disable CppCStyleCast
// ReSharper disable CppInconsistentNaming
// ReSharper disable CppClangTidyModernizeUseStdPrint
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
// ReSharper disable CppClangTidyHicppMultiwayPathsCovered
// ReSharper disable CppDefaultCaseNotHandledInSwitchStatement
// ReSharper disable CppClangTidyClangDiagnosticCastFunctionTypeStrict
// ReSharper disable CppClangTidyCertErr33C
#include <Windows.h>
#include <string>
#include <future>
#include <TlHelp32.h>
#include "pch.h"
#include "globals.h"
#include "util.h"
#include "il2cpp-init.h"
#include "il2cpp-types.h"
using Genshin::ByteArray;
HWND unityWnd = nullptr;
HANDLE hPipe = nullptr;
void* baClass;
std::string checksum;
CRITICAL_SECTION CriticalSection;
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index = 0);
namespace Hook {
ByteArray* UnityEngine_RecordUserData(const INT type) {
if (type == 0) {
const auto len = checksum.length();
const auto arr = Genshin::il2cpp_array_new_specific(baClass, len);
memcpy(&arr->vector[0], checksum.data(), len);
return arr;
}
return Genshin::il2cpp_array_new_specific(baClass, 0);
}
uint16_t BitConverter_ToUInt16(ByteArray* val, const int startIndex) {
const auto ret = CALL_ORIGIN(BitConverter_ToUInt16, val, startIndex);
if (ret == 0xAB89 && ReadMapped<UINT16>(val->vector, 2) == 24082) {
const auto headLength = ReadMapped<UINT16>(val->vector, 4);
const auto dataLength = ReadMapped<UINT32>(val->vector, 6);
const auto cStr = base64_encode(val->vector + 10 + headLength, dataLength) + "\n";
WriteFile(hPipe, cStr.c_str(), (DWORD) cStr.length(), nullptr, nullptr);
CloseHandle(hPipe);
uint16_t __fastcall BitConverter_ToUInt16(Array<uint8_t>* val, const int startIndex)
{
using namespace Globals;
const auto ToUInt16 = reinterpret_cast<decltype(&BitConverter_ToUInt16)>(Offset.BitConverter_ToUInt16);
EnterCriticalSection(&CriticalSection);
SetBreakpoint((HANDLE)-2, 0, false);
const auto ret = ToUInt16(val, startIndex);
SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true);
LeaveCriticalSection(&CriticalSection);
const auto packet = reinterpret_cast<PacketMeta*>(val->data());
auto CheckPacket = [](const PacketMeta* packet) -> bool {
const auto cmdid = _byteswap_ushort(packet->CmdId);
const auto dataLength = _byteswap_ulong(packet->DataLength);
if (dataLength < 500) {
return false;
}
if (CmdId != 0) {
return cmdid == CmdId;
}
return DynamicCmdIds.contains(cmdid);
};
using namespace Globals;
if (ret == 0xAB89 && CheckPacket(packet))
{
const auto headLength = _byteswap_ushort(packet->HeaderLength);
const auto dataLength = _byteswap_ulong(packet->DataLength);
printf("CmdId: %d\n", _byteswap_ushort(packet->CmdId));
printf("DataLength: %d\n", dataLength);
const auto base64 = Util::Base64Encode(packet->Data + headLength, dataLength) + "\n";
printf("Base64: %s\n", base64.c_str());
#ifdef _DEBUG
system("pause");
#endif
WriteFile(MessagePipe, base64.c_str(), (DWORD)base64.length(), nullptr, nullptr);
CloseHandle(MessagePipe);
ExitProcess(0);
}
return ret;
}
}
void Run(HMODULE* phModule) {
//AllocConsole();
//freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
while ((unityWnd = FindMainWindowByPID(GetCurrentProcessId())) == nullptr) {
Sleep(1000);
LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
{
using namespace Globals;
const auto exceptionRecord = ep->ExceptionRecord;
const auto contextRecord = ep->ContextRecord;
if (exceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
if (exceptionRecord->ExceptionAddress != reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
return EXCEPTION_CONTINUE_SEARCH;
}
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
Sleep(5000);
DisableVMProtect();
InitIL2CPP();
for (int i = 0; i < 3; i++) {
const auto result = Genshin::RecordUserData(i);
checksum += string(reinterpret_cast<char*>(&result->vector[0]), result->max_length);
baClass = result->klass;
return EXCEPTION_CONTINUE_SEARCH;
}
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index)
{
using namespace Globals;
if (index > 3) {
return;
}
HookManager::install(Genshin::RecordUserData, Hook::UnityEngine_RecordUserData);
HookManager::install(Genshin::BitConverter_ToUInt16, Hook::BitConverter_ToUInt16);
hPipe = CreateFile(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (hPipe == INVALID_HANDLE_VALUE) {
Win32ErrorDialog(1001);
if (!BaseAddress || Offset.BitConverter_ToUInt16 <= BaseAddress) {
// not initialized yet
return;
}
CONTEXT ctx{};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(thread, &ctx);
DWORD64* dr = &ctx.Dr0;
dr[index] = enable ? address : 0;
const auto mask = 1ull << (index * 2);
ctx.Dr7 |= mask;
SetThreadContext(thread, &ctx);
}
DWORD __stdcall ThreadProc(LPVOID hInstance)
{
#ifdef _DEBUG
AllocConsole();
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
#endif
InitializeCriticalSection(&CriticalSection);
auto initFuture = std::async(std::launch::async, InitIL2CPP);
using namespace Globals;
const auto pid = GetCurrentProcessId();
while ((GameWindow = Util::FindMainWindowByPID(pid)) == nullptr) {
SwitchToThread();
}
initFuture.get();
MessagePipe = CreateFileA(R"(\\.\pipe\YaeAchievementPipe)", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (MessagePipe == INVALID_HANDLE_VALUE)
{
#ifdef _DEBUG
printf("CreateFile failed: %d\n", GetLastError());
#else
Util::Win32ErrorDialog(1001, GetLastError());
ExitProcess(0);
#endif
}
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
while (true)
{
THREADENTRY32 te32{};
te32.dwSize = sizeof(THREADENTRY32);
const auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
for (Thread32First(hSnapshot, &te32); Thread32Next(hSnapshot, &te32);)
{
if (te32.th32OwnerProcessID != pid) {
continue;
}
if (const auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID))
{
EnterCriticalSection(&CriticalSection);
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true);
CloseHandle(hThread);
LeaveCriticalSection(&CriticalSection);
}
}
CloseHandle(hSnapshot);
Sleep(1);
}
return 0;
}
// DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall, LPVOID lpReserved) {
switch (ulReasonForCall) {
case DLL_PROCESS_ATTACH:
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Run, new HMODULE(hModule), 0, NULL);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
BOOL __stdcall DllMain(HMODULE hInstance, DWORD fdwReason, LPVOID lpReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
if (const auto hThread = CreateThread(nullptr, 0, ThreadProc, hInstance, 0, nullptr)) {
CloseHandle(hThread);
}
}
return TRUE;
}

31
lib/src/globals.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include <Windows.h>
#include <unordered_set>
#define PROPERTY2(type, name, cn, os) \
type name##_cn = cn; \
type name##_os = os; \
type get_##name() { return Globals::IsCNREL ? name##_cn : name##_os; } \
void set_##name(type value) { if (Globals::IsCNREL) name##_cn = value; else name##_os = value; } \
__declspec(property(get = get_##name, put = set_##name)) type name;
namespace Globals
{
inline HWND GameWindow = nullptr;
inline HANDLE MessagePipe = nullptr;
inline bool IsCNREL = true;
inline uintptr_t BaseAddress = 0;
// 5.1.0 - 24082
inline uint16_t CmdId = 0; // use non-zero to override dynamic search
inline std::unordered_set<uint16_t> DynamicCmdIds;
class Offsets
{
public:
PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0, 0);
//PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0x0F826CF0, 0x0F825F10); // use non-zero to override dynamic search
};
inline Offsets Offset;
}

View File

@@ -1,11 +0,0 @@
// ReSharper disable CppClangTidyBugproneMacroParentheses
#pragma once
#include "il2cpp-types.h"
// Application-specific functions
#define DO_APP_FUNC(ca, oa, r, n, p) extern r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_APP_FUNC

View File

@@ -1,9 +0,0 @@
using namespace Genshin;
// DO_APP_FUNC(CN_OFFSET, OS_OFFSET, RETURN, FUNC_NAME, (ARGS...));
DO_APP_FUNC(0x002ED400, 0x002ED390, ByteArray*, il2cpp_array_new_specific, (void* arrayTypeInfo, uint64_t length));
DO_APP_FUNC(0x0113ADC0, 0x0113AFC0, ByteArray*, RecordUserData, (int32_t nType));
DO_APP_FUNC(0x0F826CF0, 0x0F825F10, uint16_t, BitConverter_ToUInt16, (ByteArray* val, int startIndex));

View File

@@ -1,27 +1,386 @@
// ReSharper disable CppCStyleCast
// ReSharper disable CppInconsistentNaming
// ReSharper disable CppClangTidyBugproneMacroParentheses
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
#include <Windows.h>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
#include <ranges>
#include <unordered_set>
#include <unordered_map>
#include <future>
#include <mutex>
#include <immintrin.h>
#include "pch.h"
#include "globals.h"
#include "Zydis.h"
#include "util.h"
#include "il2cpp-init.h"
namespace
{
class DecodedInstruction
{
public:
DecodedInstruction() = default;
~DecodedInstruction() = default;
DecodedInstruction(const ZydisDecodedInstruction& instruction) : Instruction(instruction) {}
DecodedInstruction(const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : Instruction(instruction) {
Operands = { operands, operands + operandCount };
}
DecodedInstruction(const uint32_t rva, const ZydisDecodedInstruction& instruction, ZydisDecodedOperand* operands, uint8_t operandCount) : RVA(rva), Instruction(instruction) {
Operands = { operands, operands + operandCount };
}
// copy constructor
DecodedInstruction(const DecodedInstruction& other) = default;
// move constructor
DecodedInstruction(DecodedInstruction&& other) noexcept : RVA(other.RVA), Instruction(other.Instruction), Operands(std::move(other.Operands)) {}
uint32_t RVA = 0;
ZydisDecodedInstruction Instruction;
std::vector<ZydisDecodedOperand> Operands;
};
uintptr_t GetSection(LPCSTR name, size_t* sectionSize = nullptr)
{
using namespace Globals;
if (BaseAddress == 0)
return 0;
const auto dosHeader = (PIMAGE_DOS_HEADER)BaseAddress;
const auto ntHeader = (PIMAGE_NT_HEADERS)((uintptr_t)dosHeader + dosHeader->e_lfanew);
const auto sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
for (auto i = 0; i < ntHeader->FileHeader.NumberOfSections; i++)
{
if (strcmp((char*)sectionHeader[i].Name, name) == 0)
{
if (sectionSize != nullptr) {
*sectionSize = sectionHeader[i].Misc.VirtualSize;
}
return BaseAddress + sectionHeader[i].VirtualAddress;
}
}
return 0;
}
/// <summary>
/// decodes all instruction until next push, ignores branching
/// </summary>
/// <param name="address"></param>
/// <returns>std::vector DecodedInstruction</returns>
std::vector<DecodedInstruction> DecodeFunction(uintptr_t address)
{
using namespace Globals;
std::vector<DecodedInstruction> instructions;
ZydisDecoder decoder{};
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
ZydisDecodedInstruction instruction{};
ZydisDecoderContext context{};
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT_VISIBLE]{};
while (true)
{
const auto data = reinterpret_cast<uint8_t*>(address);
auto status = ZydisDecoderDecodeInstruction(&decoder, &context, data, ZYDIS_MAX_INSTRUCTION_LENGTH, &instruction);
if (!ZYAN_SUCCESS(status))
{
// for skipping jump tables
address += 1;
continue;
}
status = ZydisDecoderDecodeOperands(&decoder, &context, &instruction, operands, instruction.operand_count_visible);
if (!ZYAN_SUCCESS(status))
{
// for skipping jump tables
address += 1;
continue;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_PUSH && !instructions.empty()) {
break;
}
const auto rva = static_cast<uint32_t>(address - BaseAddress);
instructions.emplace_back(rva, instruction, operands, instruction.operand_count_visible);
address += instruction.length;
}
return instructions;
}
/// <summary>
/// get the count of data references in the instructions (only second oprand of mov)
/// </summary>
/// <param name="instructions"></param>
/// <returns></returns>
int32_t GetDataReferenceCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
if (instr.Instruction.mnemonic != ZYDIS_MNEMONIC_MOV)
return false;
if (instr.Operands.size() != 2)
return false;
const auto& op = instr.Operands[1];
// access to memory, based off of rip, 32-bit displacement
return op.type == ZYDIS_OPERAND_TYPE_MEMORY && op.mem.base == ZYDIS_REGISTER_RIP && op.mem.disp.has_displacement;
}));
}
int32_t GetCallCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL;
}));
}
int32_t GetUniqueCallCount(const std::vector<DecodedInstruction>& instructions)
{
std::unordered_set<uint32_t> calls;
for (const auto& instr : instructions)
{
if (instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CALL) {
uint32_t destination = instr.RVA + instr.Instruction.length + instr.Operands[0].imm.value.s;
calls.insert(destination);
}
}
return static_cast<int32_t>(calls.size());
}
int32_t GetCmpImmCount(const std::vector<DecodedInstruction>& instructions)
{
return static_cast<int32_t>(std::ranges::count_if(instructions, [](const DecodedInstruction& instr) {
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP && instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && instr.Operands[1].imm.value.u;
}));
}
void ResolveCmdId()
{
size_t sectionSize;
const auto sectionAddress = GetSection("il2cpp", &sectionSize);
const auto sectionEnd = sectionAddress + sectionSize;
printf("Section Address: 0x%llX\n", sectionAddress);
printf("Section End: 0x%llX\n", sectionEnd);
if (sectionAddress == 0)
return; // message box?
const auto candidates = Util::PatternScanAll(sectionAddress, sectionEnd, "56 48 83 EC 20 48 89 D0 48 89 CE 80 3D ? ? ? ? 00");
printf("Candidates: %llu\n", candidates.size());
std::vector<std::vector<DecodedInstruction>> candidateInstructions;
std::ranges::transform(candidates, std::back_inserter(candidateInstructions), DecodeFunction);
std::vector<std::vector<DecodedInstruction>> filteredInstructions;
std::ranges::copy_if(candidateInstructions, std::back_inserter(filteredInstructions), [](const std::vector<DecodedInstruction>& instr) {
return GetDataReferenceCount(instr) == 5 && GetCallCount(instr) == 10 && GetUniqueCallCount(instr) == 6 && GetCmpImmCount(instr) == 5;
});
// should have only one result
if (filteredInstructions.size() != 1)
{
printf("Filtered Instructions: %llu\n", filteredInstructions.size());
return;
}
const auto& instructions = filteredInstructions[0];
printf("RVA: 0x%08X\n", instructions.front().RVA);
// extract all the non-zero immediate values from the cmp instructions
std::decay_t<decltype(instructions)> cmpInstructions;
std::ranges::copy_if(instructions, std::back_inserter(cmpInstructions), [](const DecodedInstruction& instr) {
return instr.Instruction.mnemonic == ZYDIS_MNEMONIC_CMP && instr.Operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && instr.Operands[1].imm.value.u;
});
std::vector<uint32_t> cmdIds;
std::ranges::transform(cmpInstructions, std::back_inserter(cmdIds), [](const DecodedInstruction& instr) {
return instr.Operands[1].imm.value.u;
});
for (const auto& cmdId : cmdIds)
{
printf("CmdId: %u\n", cmdId);
Globals::DynamicCmdIds.insert(cmdId);
}
}
int32_t GetCallCount(uint8_t* target)
{
size_t sectionSize;
const auto sectionAddress = GetSection("il2cpp", &sectionSize);
const auto sectionEnd = sectionAddress + sectionSize;
int32_t count = 0;
const __m128i callOpcode = _mm_set1_epi8(0xE8);
const size_t simdEnd = sectionSize / 16 * 16;
for (size_t i = 0; i < simdEnd; i += 16) {
// load 16 bytes from the current address
const __m128i chunk = _mm_loadu_si128((__m128i*)(sectionAddress + i));
// compare the loaded chunk with 0xE8 in all 16 bytes
const __m128i result = _mm_cmpeq_epi8(chunk, callOpcode);
// move the comparison results into a mask
int mask = _mm_movemask_epi8(result);
while (mask != 0) {
DWORD first_match_idx = 0;
_BitScanForward(&first_match_idx, mask); // index of the first set bit (match)
// index of the instruction
const size_t instruction_index = i + first_match_idx;
const int32_t delta = *(int32_t*)(sectionAddress + instruction_index + 1);
const uintptr_t dest = sectionAddress + instruction_index + 5 + delta;
if (dest == (uintptr_t)target) {
count++;
}
// clear the bit we just processed and continue with the next match
mask &= ~(1 << first_match_idx);
}
}
return count;
}
uintptr_t FindFunctionEntry(uintptr_t address) // not a correct way to find function entry
{
__try
{
while (true)
{
// go back to 'sub rsp' instruction
uint32_t code = *(uint32_t*)address;
code &= ~0xFF000000;
if (_byteswap_ulong(code) == 0x4883EC00) { // sub rsp, ??
return address;
}
address--;
}
}
__except (1) {}
return address;
}
uintptr_t Resolve_BitConverter_ToUInt16()
{
size_t sectionSize;
const auto sectionAddress = GetSection("il2cpp", &sectionSize);
const auto sectionEnd = sectionAddress + sectionSize;
printf("Section Address: 0x%llX\n", sectionAddress);
printf("Section End: 0x%llX\n", sectionEnd);
/*
mov ecx, 0Fh
call ThrowHelper.ThrowArgumentNullException
mov ecx, 0Eh
mov edx, 16h
call ThrowHelper.ThrowArgumentOutOfRangeException
mov ecx, 5
call ThrowHelper.ThrowArgumentException
*/
auto candidates = Util::PatternScanAll(sectionAddress, sectionEnd, "B9 0F 00 00 00 E8 ? ? ? ? B9 0E 00 00 00 BA 16 00 00 00 E8 ? ? ? ? B9 05 00 00 00 E8 ? ? ? ?");
printf("Candidates: %llu\n", candidates.size());
std::vector<uintptr_t> filteredEntries;
std::ranges::copy_if(candidates, std::back_inserter(filteredEntries), [](uintptr_t& entry) {
entry = FindFunctionEntry(entry);
return entry % 16 == 0;
});
for (const auto& entry : filteredEntries)
{
printf("Entry: 0x%llX\n", entry);
}
printf("Looking for call counts...\n");
std::mutex mutex;
std::unordered_map<uintptr_t, int32_t> callCounts;
// find the call counts to candidate functions
std::vector<std::future<void>> futures;
std::ranges::transform(filteredEntries, std::back_inserter(futures), [&](uintptr_t entry) {
return std::async(std::launch::async, [&](uintptr_t e) {
const auto count = GetCallCount((uint8_t*)e);
std::lock_guard lock(mutex);
callCounts[e] = count;
}, entry);
});
for (auto& future : futures) {
future.get();
}
uintptr_t targetEntry = 0;
for (const auto& [entry, count] : callCounts)
{
printf("Entry: 0x%llX, RVA: 0x%08llX, Count: %d\n", entry, entry - Globals::BaseAddress, count);
if (count == 5) {
targetEntry = entry;
}
}
return targetEntry;
}
#define DO_APP_FUNC(ca, oa, r, n, p) r (*n) p
namespace Genshin {
#include "il2cpp-functions.h"
}
#undef DO_APP_FUNC
using std::string;
void InitIL2CPP()
{
std::string buffer;
buffer.resize(MAX_PATH);
ZeroMemory(buffer.data(), MAX_PATH);
const auto pathLength = GetModuleFileNameA(nullptr, buffer.data(), MAX_PATH);
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
buffer.resize(pathLength);
ZeroMemory(buffer.data(), pathLength);
GetModuleFileNameA(nullptr, buffer.data(), pathLength);
}
buffer.shrink_to_fit();
void InitIL2CPP() {
TCHAR szFileName[MAX_PATH];
GetModuleFileName(nullptr, szFileName, MAX_PATH);
_strupr_s(szFileName);
const auto isCN = strstr(szFileName, "YUANSHEN.EXE");
const auto uBase = reinterpret_cast<uint64_t>(GetModuleHandle(isCN ? "YuanShen.exe" : "GenshinImpact.exe"));
#define DO_APP_FUNC(ca, oa, r, n, p) n = (r (*) p)(uBase + (isCN ? ca : oa))
#include "il2cpp-functions.h"
#undef DO_APP_FUNC
using namespace Globals;
IsCNREL = buffer.find("YuanShen.exe") != std::string::npos;
BaseAddress = (uintptr_t)GetModuleHandleA(nullptr);
std::future<void> resolveFuncFuture = std::async(std::launch::async, [] {
if (Offset.BitConverter_ToUInt16 != 0) {
Offset.BitConverter_ToUInt16 += BaseAddress;
}
else {
Offset.BitConverter_ToUInt16 = Resolve_BitConverter_ToUInt16();
}
});
std::future<void> resolveCmdIdFuture = std::async(std::launch::async, [] {
if (CmdId == 0) {
ResolveCmdId();
}
});
resolveFuncFuture.get();
resolveCmdIdFuture.get();
printf("BaseAddress: 0x%llX\n", BaseAddress);
printf("IsCNREL: %d\n", IsCNREL);
printf("BitConverter_ToUInt16: 0x%llX\n", Offset.BitConverter_ToUInt16);
}

View File

@@ -1,15 +1,38 @@
// ReSharper disable CppClangTidyClangDiagnosticReservedIdentifier
// ReSharper disable CppClangTidyBugproneReservedIdentifier
#pragma once
#include <cstdint>
namespace Genshin {
template <typename T>
class Array
{
public:
void* klass;
void* monitor;
void* bounds;
size_t max_length;
T vector[1];
struct ByteArray {
void* klass;
void* monitor;
void* bounds;
uint64_t max_length;
uint8_t vector[32];
};
}
Array() = delete;
T* data() {
return vector;
}
};
static_assert(alignof(Array<uint8_t>) == 8, "Array alignment is incorrect");
static_assert(offsetof(Array<uint8_t>, vector) == 32, "vector offset is incorrect");
#pragma pack(push, 1)
struct PacketMeta
{
uint16_t HeadMagic;
uint16_t CmdId;
uint16_t HeaderLength;
uint32_t DataLength;
uint8_t Data[1];
};
#pragma pack(pop)
static_assert(offsetof(PacketMeta, CmdId) == 2, "CmdId offset is incorrect");
static_assert(offsetof(PacketMeta, HeaderLength) == 4, "HeadLength offset is incorrect");
static_assert(offsetof(PacketMeta, DataLength) == 6, "DataLength offset is incorrect");
static_assert(offsetof(PacketMeta, Data) == 10, "Data offset is incorrect");

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,23 +0,0 @@
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>
// 添加要在此处预编译的标头
#include <map>
#include <string>
#include <cstdint>
#include <detours.h>
#include "HookManager.h"
#include "il2cpp-appdata.h"
#endif //PCH_H

View File

@@ -1,93 +1,182 @@
#include "pch.h"
#include <string>
#include "util.h"
#include "globals.h"
VOID DisableVMProtect() {
DWORD oldProtect = 0;
auto ntdll = GetModuleHandleA("ntdll.dll");
auto pNtProtectVirtualMemory = GetProcAddress(ntdll, "NtProtectVirtualMemory");
auto pNtQuerySection = GetProcAddress(ntdll, "NtQuerySection");
DWORD old;
VirtualProtect(pNtProtectVirtualMemory, 1, PAGE_EXECUTE_READWRITE, &old);
*(uintptr_t*)pNtProtectVirtualMemory = *(uintptr_t*)pNtQuerySection & ~(0xFFui64 << 32) | (uintptr_t)(*(uint32_t*)((uintptr_t)pNtQuerySection + 4) - 1) << 32;
VirtualProtect(pNtProtectVirtualMemory, 1, old, &old);
}
#pragma region ByteUtils
bool IsLittleEndian() {
UINT i = 1;
char* c = (char*)&i;
return *c;
}
#pragma endregion
#ifdef _DEBUG
#pragma runtime_checks("", off)
#endif
#pragma region FindMainWindowByPID
struct HandleData {
DWORD pid;
HWND hwnd;
};
namespace
{
struct HandleData {
DWORD pid;
HWND hwnd;
};
BOOL IsMainWindow(HWND handle) {
return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle) == TRUE;
}
bool IsMainWindow(HWND handle) {
return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle) == TRUE;
}
BOOL IsUnityWindow(HWND handle) {
TCHAR name[256];
GetClassName(handle, name, 256);
return _strcmpi(name, "UnityWndClass") == 0;
}
bool IsUnityWindow(HWND handle) {
char szName[256]{};
GetClassNameA(handle, szName, 256);
return _stricmp(szName, "UnityWndClass") == 0;
}
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
HandleData& data = *(HandleData*)lParam;
DWORD pid = 0;
GetWindowThreadProcessId(handle, &pid);
if (data.pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
return TRUE;
data.hwnd = handle;
return FALSE;
}
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
HandleData& data = *(HandleData*)lParam;
DWORD pid = 0;
GetWindowThreadProcessId(handle, &pid);
if (data.pid != pid || !IsMainWindow(handle) || !IsUnityWindow(handle))
return TRUE;
data.hwnd = handle;
return FALSE;
}
std::tuple<std::vector<uint8_t>, std::vector<bool>> PatternToBytes(const char* pattern)
{
std::vector<uint8_t> bytes;
std::vector<bool> maskBytes;
const auto start = const_cast<char*>(pattern);
const auto end = const_cast<char*>(pattern) + strlen(pattern);
for (auto current = start; current < end; ++current) {
if (*current == '?') {
++current;
if (*current == '?')
++current;
bytes.push_back(-1);
maskBytes.push_back(false);
}
else {
bytes.push_back(strtoul(current, &current, 16));
maskBytes.push_back(true);
}
}
return { bytes, maskBytes };
}
HWND FindMainWindowByPID(DWORD pid) {
HandleData data = { pid, 0 };
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
return data.hwnd;
}
#pragma endregion
static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static constexpr LPCSTR base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64_encode(BYTE const* buf, unsigned int bufLen) {
std::string ret;
int i = 0;
BYTE char_array_3[3];
BYTE char_array_4[4];
while (bufLen--) {
char_array_3[i++] = *buf++;
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
int j;
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; j < i + 1; j++)
ret += base64_chars[char_array_4[j]];
while (i++ < 3)
ret += '=';
}
return ret;
namespace Util
{
HWND FindMainWindowByPID(DWORD pid)
{
HandleData data = { pid, 0 };
EnumWindows(EnumWindowsCallback, (LPARAM)&data);
return data.hwnd;
}
std::string Base64Encode(BYTE const* buf, unsigned int bufLen)
{
std::string ret;
int i = 0;
BYTE char_array_3[3];
BYTE char_array_4[4];
while (bufLen--) {
char_array_3[i++] = *buf++;
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
int j;
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; j < i + 1; j++)
ret += base64_chars[char_array_4[j]];
while (i++ < 3)
ret += '=';
}
return ret;
}
void ErrorDialog(LPCSTR title, LPCSTR msg)
{
MessageBoxA(Globals::GameWindow, msg, title, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL);
}
void ErrorDialog(LPCSTR msg)
{
ErrorDialog("YaeAchievement", msg);
}
void Win32ErrorDialog(DWORD code, DWORD winerrcode)
{
const std::string msg = "CRITICAL ERROR!\nError code: " + std::to_string(winerrcode) + "-" + std::to_string(code) +
"\n\nPlease take the screenshot and contact developer by GitHub Issue to solve this problem\nNOT MIHOYO/COGNOSPHERE CUSTOMER SERVICE!";
ErrorDialog("YaeAchievement", msg.c_str());
}
uintptr_t PatternScan(uintptr_t start, uintptr_t end, const char* pattern)
{
const auto [patternBytes, patternMask] = PatternToBytes(pattern);
const auto scanBytes = reinterpret_cast<uint8_t*>(start);
const auto patternSize = patternBytes.size();
const auto pBytes = patternBytes.data();
for (auto i = 0ul; i < end - start - patternSize; ++i) {
bool found = true;
for (auto j = 0ul; j < patternSize; ++j) {
if (scanBytes[i + j] != pBytes[j] && patternMask[j]) {
found = false;
break;
}
}
if (found) {
return reinterpret_cast<uintptr_t>(&scanBytes[i]);
}
}
return 0;
}
std::vector<uintptr_t> PatternScanAll(uintptr_t start, uintptr_t end, const char* pattern)
{
std::vector<uintptr_t> results;
const auto [patternBytes, patternMask] = PatternToBytes(pattern);
const auto scanBytes = reinterpret_cast<uint8_t*>(start);
const auto patternSize = patternBytes.size();
const auto pBytes = patternBytes.data();
for (auto i = 0ul; i < end - start - patternSize; ++i) {
bool found = true;
for (auto j = 0ul; j < patternSize; ++j) {
if (scanBytes[i + j] != pBytes[j] && patternMask[j]) {
found = false;
break;
}
}
if (found) {
results.push_back(reinterpret_cast<uintptr_t>(&scanBytes[i]));
i += patternSize - 1;
}
}
return results;
}
}
#ifdef _DEBUG
#pragma runtime_checks("", restore)
#endif

View File

@@ -1,25 +1,18 @@
// ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken
#pragma once
#include <Windows.h>
#include <type_traits>
#include <vector>
using std::string;
namespace Util
{
HWND FindMainWindowByPID(DWORD pid);
std::string Base64Encode(BYTE const* buf, unsigned int bufLen);
VOID DisableVMProtect();
bool IsLittleEndian();
HWND FindMainWindowByPID(DWORD pid);
std::string base64_encode(BYTE const* buf, unsigned int bufLen);
void ErrorDialog(LPCSTR title, LPCSTR msg);
void ErrorDialog(LPCSTR msg);
void Win32ErrorDialog(DWORD code, DWORD winerrcode);
#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())
template<class T>
static T ReadMapped(void* data, int offset, bool littleEndian = false) {
char* cData = (char*)data;
T result = {};
if (IsLittleEndian() != littleEndian) {
for (int i = 0; i < sizeof(T); i++)
((char*)&result)[i] = cData[offset + sizeof(T) - i - 1];
return result;
}
memcpy(&result, cData + offset, sizeof(result));
return result;
}
uintptr_t PatternScan(uintptr_t start, uintptr_t end, const char* pattern);
std::vector<uintptr_t> PatternScanAll(uintptr_t start, uintptr_t end, const char* pattern);
}

99
res/App.Designer.cs generated
View File

@@ -9,8 +9,8 @@
namespace YaeAchievement.res {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
@@ -22,15 +22,15 @@ namespace YaeAchievement.res {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class App {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal App() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
@@ -44,7 +44,7 @@ namespace YaeAchievement.res {
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
@@ -58,7 +58,7 @@ namespace YaeAchievement.res {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to all achievement.
/// </summary>
@@ -67,7 +67,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AllAchievement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please close another instance..
/// </summary>
@@ -76,7 +76,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AnotherInstance", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to YaeAchievement ({0}).
/// </summary>
@@ -85,7 +85,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("AppBanner", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You need to login genshin impact before exporting..
/// </summary>
@@ -94,7 +94,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ConfigNeedStartGenshin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Download: {0}.
/// </summary>
@@ -103,7 +103,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("DownloadLink", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Network error ({0}: {1}).
/// </summary>
@@ -112,9 +112,9 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExceptionNetwork", resourceCulture);
}
}
/// <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
@@ -130,7 +130,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportChoose", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fail, please contact developer to get help information.
/// </summary>
@@ -139,7 +139,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToCocogoatFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to cocogoat..
/// </summary>
@@ -148,7 +148,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToCocogoatSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to {0}.
/// </summary>
@@ -157,7 +157,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToFileSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update Snap Hutao and retry..
/// </summary>
@@ -166,7 +166,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToSnapGenshinNeedUpdate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to Snap Hutao..
/// </summary>
@@ -175,7 +175,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToSnapGenshinSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please launch/update Teyvat Guide and retry..
/// </summary>
@@ -184,7 +184,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToTauriFail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to Teyvat Guide..
/// </summary>
@@ -193,7 +193,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToTauriSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.
/// </summary>
@@ -202,7 +202,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToWxApp1Success", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update xunkong and retry..
/// </summary>
@@ -211,7 +211,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToXunkongNeedUpdate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully exported to xunkong..
/// </summary>
@@ -220,7 +220,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("ExportToXunkongSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game process start ({0}).
/// </summary>
@@ -229,7 +229,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GameLoading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game exited..
/// </summary>
@@ -238,7 +238,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GameProcessExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update genshin and retry..
/// </summary>
@@ -247,7 +247,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GenshinHashError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please close game before run this application. ({0}).
/// </summary>
@@ -256,7 +256,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("GenshinIsRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Network error:.
/// </summary>
@@ -265,7 +265,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("NetworkError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No write permission on {0}..
/// </summary>
@@ -274,7 +274,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("NoWritePermission", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Press any key to exit..
/// </summary>
@@ -283,7 +283,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("PressKeyToExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reward not taken.
/// </summary>
@@ -292,7 +292,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusFinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid.
/// </summary>
@@ -301,7 +301,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusInvalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished.
/// </summary>
@@ -310,7 +310,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusRewardTaken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unfinished.
/// </summary>
@@ -319,9 +319,9 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("StatusUnfinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Description:
/// Looks up a localized string similar to Description:
///{0}.
/// </summary>
internal static string UpdateDescription {
@@ -329,7 +329,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloading update package....
/// </summary>
@@ -338,7 +338,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateDownloading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Has update: {0} =&gt; {1}.
/// </summary>
@@ -347,7 +347,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UpdateNewVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
@@ -357,7 +357,7 @@ namespace YaeAchievement.res {
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized string similar to Upload error to appcenter....
/// </summary>
@@ -366,7 +366,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UploadError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use previous fetched data? (yes|no).
/// </summary>
@@ -375,7 +375,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("UsePreviousData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloading Visual C++ Redistributable....
/// </summary>
@@ -384,7 +384,7 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("VcRuntimeDownload", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installing Visual C++ Redistributable....
/// </summary>
@@ -393,5 +393,14 @@ namespace YaeAchievement.res {
return ResourceManager.GetString("VcRuntimeInstalling", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please update game and retry..
/// </summary>
internal static string WaitMetadataUpdate {
get {
return ResourceManager.GetString("WaitMetadataUpdate", resourceCulture);
}
}
}
}

View File

@@ -137,4 +137,7 @@ Input a number (0-8): </value>
<data name="ExportToTauriFail" xml:space="preserve">
<value>Please launch/update Teyvat Guide and retry.</value>
</data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>Please update game and retry.</value>
</data>
</root>

View File

@@ -127,4 +127,7 @@
<data name="ExportToTauriSuccess" xml:space="preserve">
<value>在 Teyvat Guide 进行下一步操作</value>
</data>
<data name="WaitMetadataUpdate" xml:space="preserve">
<value>当前元数据版本不匹配,请更新原神至最新版本或等待元数据更新后重试。</value>
</data>
</root>

View File

@@ -1,21 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Proto";
message Achievement {
enum Status {
INVALID = 0;
UNFINISHED = 1;
FINISHED = 2;
REWARD_TAKEN = 3;
}
uint32 timestamp = 11;
uint32 current = 5;
uint32 total = 15;
uint32 id = 2;
Status status = 7;
}
message AchievementAllDataNotify {
repeated Achievement list = 4;
}

View File

@@ -2,6 +2,14 @@ syntax = "proto3";
option csharp_namespace = "Proto";
message AchievementProtoFieldInfo {
uint32 id = 1;
uint32 status = 2;
uint32 total_progress = 3;
uint32 current_progress = 4;
uint32 finish_timestamp = 5;
}
message AchievementItem {
uint32 pre = 1;
uint32 group = 2;
@@ -13,4 +21,5 @@ message AchievementInfo {
string version = 1;
map<uint32, string> group = 2;
map<uint32, AchievementItem> items = 3;
AchievementProtoFieldInfo pb_info = 4;
}

View File

@@ -3,13 +3,13 @@ syntax = "proto3";
option csharp_namespace = "Proto";
message UpdateInfo {
uint32 versionCode = 1;
string versionName = 2;
uint32 version_code = 1;
string version_name = 2;
string description = 3;
string packageLink = 4;
bool forceUpdate = 5;
bool enableLibDownload = 6;
bool enableAutoDownload = 7;
string currentCNGameHash = 8;
string currentOSGameHash = 9;
string package_link = 4;
bool force_update = 5;
bool enable_lib_download = 6;
bool enable_auto_update = 7;
string current_cn_hash = 8;
string current_os_hash = 9;
}

View File

@@ -5,9 +5,8 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Win32;
using Proto;
using YaeAchievement.Parsers;
using YaeAchievement.res;
using static Proto.Achievement.Types;
namespace YaeAchievement;
@@ -120,10 +119,10 @@ public static class Export {
}
private static void ToPaimon(AchievementAllDataNotify data) {
var info = LoadAchievementInfo().Items.ToDictionary(pair => pair.Key, pair => pair.Value.Group);
var info = GlobalVars.AchievementInfo.Items.ToDictionary(pair => pair.Key, pair => pair.Value.Group);
var final = new Dictionary<string, Dictionary<uint, Dictionary<uint, bool>>> {
["achievement"] = data.List
.Where(achievement => achievement.Status is Status.Finished or Status.RewardTaken)
["achievement"] = data.AchievementList
.Where(achievement => achievement.Status is AchievementStatus.Finished or AchievementStatus.RewardTaken)
.Where(achievement => info.ContainsKey(achievement.Id))
.GroupBy(achievement => info[achievement.Id], achievement => achievement.Id)
.OrderBy(group => group.Key)
@@ -137,7 +136,7 @@ public static class Export {
private static void ToSeelie(AchievementAllDataNotify data) {
var output = new Dictionary<uint, Dictionary<string, bool>>();
foreach (var ach in data.List.Where(a => a.Status is Status.Finished or Status.RewardTaken)) {
foreach (var ach in data.AchievementList.Where(a => a.Status is AchievementStatus.Finished or AchievementStatus.RewardTaken)) {
output[ach.Id] = new Dictionary<string, bool> {
["done"] = true
};
@@ -153,23 +152,25 @@ public static class Export {
// ReSharper disable once InconsistentNaming
private static void ToCSV(AchievementAllDataNotify data) {
var info = LoadAchievementInfo();
var info = GlobalVars.AchievementInfo;
var outList = new List<List<object>>();
foreach (var ach in data.List.OrderBy(a => a.Id)) {
foreach (var ach in data.AchievementList.OrderBy(a => a.Id)) {
if (UnusedAchievement.Contains(ach.Id)) continue;
if (!info.Items.TryGetValue(ach.Id, out var achInfo) || achInfo == null) {
Console.WriteLine($@"Unable to find {ach.Id} in metadata.");
continue;
}
var finishAt = "";
if (ach.Timestamp != 0) {
var ts = Convert.ToInt64(ach.Timestamp);
if (ach.FinishTimestamp != 0) {
var ts = Convert.ToInt64(ach.FinishTimestamp);
finishAt = DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
}
var current = ach.Status != Status.Unfinished ? ach.Current == 0 ? ach.Total : ach.Current : ach.Current;
var current = ach.Status != AchievementStatus.Unfinished
? ach.CurrentProgress == 0 ? ach.TotalProgress : ach.CurrentProgress
: ach.CurrentProgress;
outList.Add([
ach.Id, ach.Status.ToDesc(), achInfo.Group, achInfo.Name,
achInfo.Description, current, ach.Total, finishAt
achInfo.Description, current, ach.TotalProgress, finishAt
]);
}
var output = new List<string> { "ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间" };
@@ -194,13 +195,13 @@ public static class Export {
// ReSharper disable once InconsistentNaming
private static Dictionary<string, object> ExportToUIAFApp(AchievementAllDataNotify data) {
var output = data.List
.Where(a => (uint)a.Status > 1 || a.Current > 0)
var output = data.AchievementList
.Where(a => (uint)a.Status > 1 || a.CurrentProgress > 0)
.Select(ach => new Dictionary<string, uint> {
["id"] = ach.Id,
["status"] = (uint) ach.Status,
["current"] = ach.Current,
["timestamp"] = ach.Timestamp
["current"] = ach.CurrentProgress,
["timestamp"] = ach.FinishTimestamp
})
.ToList();
return new Dictionary<string, object> {
@@ -225,21 +226,16 @@ public static class Export {
private static readonly List<uint> UnusedAchievement = [ 84517 ];
private static string ToDesc(this Status status) {
private static string ToDesc(this AchievementStatus status) {
return status switch {
Status.Invalid => App.StatusInvalid,
Status.Finished => App.StatusFinished,
Status.Unfinished => App.StatusUnfinished,
Status.RewardTaken => App.StatusRewardTaken,
AchievementStatus.Invalid => App.StatusInvalid,
AchievementStatus.Finished => App.StatusFinished,
AchievementStatus.Unfinished => App.StatusUnfinished,
AchievementStatus.RewardTaken => App.StatusRewardTaken,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
}
private static AchievementInfo LoadAchievementInfo() {
var b = Utils.GetBucketFileAsByteArray("schicksal/metadata");
return AchievementInfo.Parser.ParseFrom(b);
}
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
// ReSharper disable once LocalizableElement
Console.WriteLine($"{msg}: {ex.Message}");

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Proto;
namespace YaeAchievement;
@@ -20,14 +21,17 @@ public static class GlobalVars {
public static readonly string CachePath = Path.Combine(DataPath, "cache");
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
public const uint AppVersionCode = 51;
public const string AppVersionName = "4.1";
public const uint AppVersionCode = 234;
public const string AppVersionName = "5.2";
public const string PipeName = "YaeAchievementPipe";
public const string BucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
public static AchievementInfo AchievementInfo { get; }
static GlobalVars() {
Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(CachePath);
AchievementInfo = AchievementInfo.Parser.ParseFrom(Utils.GetBucketFileAsByteArray("schicksal/metadata"));
}
}

0
src/Outputs/.gitkeep Normal file
View File

View File

@@ -0,0 +1,111 @@
using System.Runtime.CompilerServices;
using Google.Protobuf;
using YaeAchievement.res;
namespace YaeAchievement.Parsers;
public enum AchievementStatus {
Invalid,
Unfinished,
Finished,
RewardTaken,
}
public class AchievementItem {
public uint Id { get; init; }
public uint TotalProgress { get; init; }
public uint CurrentProgress { get; init; }
public uint FinishTimestamp { get; init; }
public AchievementStatus Status { get; init; }
}
public class AchievementAllDataNotify {
public List<AchievementItem> AchievementList { get; private init; } = [];
public static AchievementAllDataNotify ParseFrom(byte[] bytes) {
using var stream = new CodedInputStream(bytes);
var data = new List<Dictionary<uint, uint>>();
var errTimes = 0;
try {
uint tag;
while ((tag = stream.ReadTag()) != 0) {
if ((tag & 7) == 2) { // is LengthDelimited
var dict = new Dictionary<uint, uint>();
using var eStream = new CodedInputStream(ReadRawBytes(stream, stream.ReadLength()));
try {
while ((tag = eStream.ReadTag()) != 0) {
if ((tag & 7) != 0) { // not VarInt
dict = null;
break;
}
dict[tag >> 3] = eStream.ReadUInt32();
}
if (dict != null) {
data.Add(dict);
}
} catch (InvalidProtocolBufferException) {
if (errTimes++ > 0) { // allows 1 fail on 'reward_taken_goal_id_list'
throw;
}
}
}
}
} catch (InvalidProtocolBufferException) {
// ReSharper disable once LocalizableElement
Console.WriteLine("Parse failed");
File.WriteAllBytes("achievement_raw_data.bin", bytes);
Environment.Exit(0);
}
if (data.Count == 0) {
return new AchievementAllDataNotify();
}
uint tId, sId, iId, currentId, totalId;
if (data.Count > 20) { /* uwu */
(tId, var cnt) = data // ↓ 2020-09-15 04:15:14
.GroupKeys(value => value > 1600114514).Select(g => (g.Key, g.Count())).MaxBy(p => p.Item2);
sId = data // FINISHED ↓ ↓ REWARD_TAKEN
.GroupKeys(value => value is 2 or 3).First(g => g.Count() == cnt).Key;
iId = data // ↓ id: 8xxxx
.GroupKeys(value => value / 10000 % 10 == 8).MaxBy(g => g.Count())!.Key;
(currentId, totalId) = data
.Where(d => d[sId] is 2 or 3)
.Select(d => d.ToDictionary().RemoveValues(tId, sId, iId).ToArray())
.Where(d => d.Length == 2 && d[0].Value != d[1].Value)
.GroupBy(a => a[0].Value > a[1].Value ? (a[0].Key, a[1].Key) : (a[1].Key, a[0].Key))
.Select(g => (FieldIds: g.Key, Count: g.Count()))
.MaxBy(p => p.Count)
.FieldIds;
#if DEBUG
// ReSharper disable once LocalizableElement
Console.WriteLine($"Id={iId}, Status={sId}, Total={totalId}, Current={currentId}, Timestamp={tId}");
#endif
} else {
var info = GlobalVars.AchievementInfo.PbInfo; // ...
iId = info.Id;
tId = info.FinishTimestamp;
sId = info.Status;
totalId = info.TotalProgress;
currentId = info.CurrentProgress;
if (data.Any(dict => !dict.ContainsKey(iId) || !dict.ContainsKey(sId) || !dict.ContainsKey(totalId))) {
Console.WriteLine(App.WaitMetadataUpdate);
Environment.Exit(0);
}
}
return new AchievementAllDataNotify {
AchievementList = data.Select(dict => new AchievementItem {
Id = dict[iId],
Status = (AchievementStatus) dict[sId],
TotalProgress = dict[totalId],
CurrentProgress = dict.GetValueOrDefault(currentId),
FinishTimestamp = dict.GetValueOrDefault(tId),
}).ToList()
};
}
[UnsafeAccessor(UnsafeAccessorKind.Method)]
private static extern byte[] ReadRawBytes(CodedInputStream stream, int size);
}

View File

@@ -1,6 +1,6 @@
using System.Text;
using Proto;
using YaeAchievement;
using YaeAchievement.Parsers;
using YaeAchievement.res;
using static YaeAchievement.Utils;
@@ -29,7 +29,7 @@ var historyCache = new CacheFile("ExportData");
AchievementAllDataNotify? data = null;
try {
data = AchievementAllDataNotify.Parser.ParseFrom(historyCache.Read().Content);
data = AchievementAllDataNotify.ParseFrom(historyCache.Read().Content.ToByteArray());
} catch (Exception) { /* ignored */ }
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) {
@@ -43,7 +43,7 @@ if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null)
StartAndWaitResult(AppConfig.GamePath, str => {
GlobalVars.UnexpectedExit = false;
var bytes = Convert.FromBase64String(str);
var list = AchievementAllDataNotify.Parser.ParseFrom(bytes);
var list = AchievementAllDataNotify.ParseFrom(bytes);
historyCache.Write(bytes);
Export.Choose(list);
return true;

View File

@@ -0,0 +1,16 @@
// ReSharper disable once CheckNamespace
namespace System.Collections.Generic;
public static class Collection {
public static IDictionary<TKey, TValue> RemoveValues<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary,
params TKey[] keys
) {
foreach (var key in keys) {
dictionary.Remove(key);
}
return dictionary;
}
}

View File

@@ -0,0 +1,14 @@
// ReSharper disable once CheckNamespace
namespace System.Linq;
public static class Enumerable {
public static IEnumerable<IGrouping<TKey, TKey>> GroupKeys<TKey, TValue>(
this IEnumerable<Dictionary<TKey, TValue>> source,
Func<TValue, bool> condition
) where TKey : notnull {
return source.SelectMany(dict => dict.Where(pair => condition(pair.Value)).Select(pair => pair.Key)).GroupBy(x => x);
}
}

View File

@@ -93,7 +93,7 @@ public static class Utils {
if (GlobalVars.AppVersionCode < info.VersionCode) {
Console.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, info.VersionName);
Console.WriteLine(App.UpdateDescription, info.Description);
if (info.EnableAutoDownload) {
if (info.EnableAutoUpdate) {
Console.WriteLine(App.UpdateDownloading);
var tmpPath = Path.GetTempFileName();
File.WriteAllBytes(tmpPath, GetBucketFileAsByteArray(info.PackageLink));
@@ -205,22 +205,8 @@ public static class Utils {
};
}
private static bool CheckGenshinIsLatestVersion(string path) {
#if DEBUG
return true;
#else
if (!File.Exists(path)) return false;
var hash = File.ReadAllBytes(path).MD5Hash();
return hash == _updateInfo.CurrentCNGameHash || hash == _updateInfo.CurrentOSGameHash;
#endif
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static Thread StartAndWaitResult(string exePath, Func<string, bool> onReceive) {
if (!CheckGenshinIsLatestVersion(exePath)) {
Console.WriteLine(App.GenshinHashError);
Environment.Exit(0);
}
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
try {
File.Delete(GlobalVars.LibFilePath);