[skip ci] support virtual item

This commit is contained in:
HolographicHat
2025-01-20 17:32:47 +08:00
parent 35773f49f4
commit e7d21865c7
14 changed files with 292 additions and 55 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>

View File

@@ -63,6 +63,10 @@ message Furniture {
uint32 count = 1;
}
message VirtualItem {
int64 count = 1;
}
message Item {
uint32 item_id = 1;
uint64 guid = 2;
@@ -70,5 +74,6 @@ message Item {
Material material = 5;
Equip equip = 6;
Furniture furniture = 7;
VirtualItem virtual_item = 255;
}
}

View File

@@ -11,7 +11,6 @@ namespace YaeAchievement;
public static class GlobalVars {
public static bool UnexpectedExit { get; set; } = true;
public static bool PauseOnExit { get; set; } = true;
public static Version AppVersion { get; } = Assembly.GetEntryAssembly()!.GetName().Version!;

View File

@@ -25,23 +25,24 @@ public static class Injector {
}
// todo: refactor
public static unsafe int LoadLibraryAndInject(HANDLE hProc, ReadOnlySpan<byte> libPath) {
public static unsafe int LoadLibraryAndInject(HANDLE hProc, ReadOnlySpan<char> libPath) {
fixed (char* lpModelName = "kernel32.dll") {
var hKernel = Native.GetModuleHandle(lpModelName);
if (hKernel.IsNull) {
return new Win32Exception().PrintMsgAndReturnErrCode("GetModuleHandle fail");
}
fixed(byte* lpProcName = "LoadLibraryA"u8) {
fixed(byte* lpProcName = "LoadLibraryW"u8) {
var pLoadLibrary = Native.GetProcAddress(hKernel, (PCSTR)lpProcName);
if (pLoadLibrary.IsNull) {
return new Win32Exception().PrintMsgAndReturnErrCode("GetProcAddress fail");
}
var pBase = Native.VirtualAllocEx(hProc, default, unchecked((uint)libPath.Length + 1), VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
var libPathByteLen = (uint) libPath.Length * 2;
var pBase = Native.VirtualAllocEx(hProc, default, libPathByteLen + 2, VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
if ((nint)pBase == 0) {
return new Win32Exception().PrintMsgAndReturnErrCode("VirtualAllocEx fail");
}
fixed (void* lpBuffer = libPath) {
if (!Native.WriteProcessMemory(hProc, pBase, lpBuffer, unchecked((uint)libPath.Length))) {
if (!Native.WriteProcessMemory(hProc, pBase, lpBuffer, libPathByteLen)) {
return new Win32Exception().PrintMsgAndReturnErrCode("WriteProcessMemory fail");
}
}

View File

@@ -25,13 +25,23 @@ public class AchievementItem {
public class AchievementAllDataNotify {
public List<AchievementItem> AchievementList { get; private init; } = [];
private static AchievementAllDataNotify? Instance { get; set; }
public static bool OnReceive(byte[] bytes) {
public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32());
GlobalVars.AchievementDataCache.Write(bytes);
Export.Choose(ParseFrom(bytes));
Instance = ParseFrom(bytes);
return true;
}
public static void OnFinish() {
if (Instance == null) {
throw new ApplicationException("No data received");
}
Export.Choose(Instance);
}
public static AchievementAllDataNotify ParseFrom(byte[] bytes) {
using var stream = new CodedInputStream(bytes);
var data = new List<Dictionary<uint, uint>>();

View File

@@ -0,0 +1,106 @@
using Proto;
using static YaeAchievement.Parsers.PropType;
namespace YaeAchievement.Parsers;
public enum PropType {
None = 0,
Exp = 1001,
BreakLevel = 1002,
SatiationVal = 1003,
SatiationPenaltyTime = 1004,
GearStartVal = 2001,
GearStopVal = 2002,
Level = 4001,
LastChangeAvatarTime = 10001,
MaxSpringVolume = 10002,
CurSpringVolume = 10003,
IsSpringAutoUse = 10004,
SpringAutoUsePercent = 10005,
IsFlyable = 10006,
IsWeatherLocked = 10007,
IsGameTimeLocked = 10008,
IsTransferable = 10009,
MaxStamina = 10010,
CurPersistStamina = 10011,
CurTemporaryStamina = 10012,
PlayerLevel = 10013,
PlayerExp = 10014,
PlayerHCoin = 10015,
PlayerSCoin = 10016,
PlayerMpSettingType = 10017,
IsMpModeAvailable = 10018,
PlayerWorldLevel = 10019,
PlayerResin = 10020,
PlayerWaitSubHCoin = 10022,
PlayerWaitSubSCoin = 10023,
IsOnlyMpWithPsPlayer = 10024,
PlayerMCoin = 10025,
PlayerWaitSubMCoin = 10026,
PlayerLegendaryKey = 10027,
IsHasFirstShare = 10028,
PlayerForgePoint = 10029,
CurClimateMeter = 10035,
CurClimateType = 10036,
CurClimateAreaId = 10037,
CurClimateAreaClimateType = 10038,
PlayerWorldLevelLimit = 10039,
PlayerWorldLevelAdjustCd = 10040,
PlayerLegendaryDailyTaskNum = 10041,
PlayerHomeCoin = 10042,
PlayerWaitSubHomeCoin = 10043,
IsAutoUnlockSpecificEquip = 10044,
PlayerGCGCoin = 10045,
PlayerWaitSubGCGCoin = 10046,
PlayerOnlineTime = 10047,
IsDiveable = 10048,
MaxDiveStamina = 10049,
CurPersistDiveStamina = 10050,
IsCanPutFiveStarReliquary = 10051,
IsAutoLockFiveStarReliquary = 10052,
PlayerRoleCombatCoin = 10053,
CurPhlogiston = 10054,
ReliquaryTemporaryExp = 10055,
IsMpCrossPlatformEnabled = 10056,
IsOnlyMpWithPlatformPlayer = 10057,
PlayerMusicGameBookCoin = 10058,
IsNotShowReliquaryRecommendProp = 10059,
}
public static class PlayerPropNotify {
private static readonly Dictionary<PropType, double> PropMap = [];
public static bool OnReceive(BinaryReader reader) {
var propType = (PropType) reader.ReadInt32();
var propValue = reader.ReadDouble();
PropMap.Add(propType, propValue);
return false;
}
public static void OnFinish() {
PlayerStoreNotify.Instance.ItemList.AddRange([
CreateVirtualItem(201, GetPropValue(PlayerHCoin) - GetPropValue(PlayerWaitSubHCoin)),
CreateVirtualItem(202, GetPropValue(PlayerSCoin) - GetPropValue(PlayerWaitSubSCoin)),
CreateVirtualItem(203, GetPropValue(PlayerMCoin) - GetPropValue(PlayerWaitSubMCoin)),
CreateVirtualItem(204, GetPropValue(PlayerHomeCoin) - GetPropValue(PlayerWaitSubHomeCoin)),
CreateVirtualItem(206, GetPropValue(PlayerRoleCombatCoin)),
CreateVirtualItem(207, GetPropValue(PlayerMusicGameBookCoin)),
]);
}
private static Item CreateVirtualItem(uint id, double count) {
return new Item {
ItemId = id,
VirtualItem = new VirtualItem {
Count = (long) count
}
};
}
private static double GetPropValue(PropType propType) {
return PropMap.GetValueOrDefault(propType);
}
}

View File

@@ -1,7 +1,11 @@
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf;
using Proto;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable CollectionNeverQueried.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
namespace YaeAchievement.Parsers;
public class PlayerStoreNotify {
@@ -12,19 +16,16 @@ public class PlayerStoreNotify {
public List<Item> ItemList { get; set; } = [];
public static bool OnReceive(byte[] bytes) {
#if DEBUG
var ntf = ParseFrom(bytes);
File.WriteAllText("store_data.json", JsonSerializer.Serialize(ntf, new JsonSerializerOptions {
WriteIndented = true
}));
#endif
public static PlayerStoreNotify Instance { get; } = new ();
public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32());
Instance.ParseFrom(bytes);
return true;
}
private static PlayerStoreNotify ParseFrom(byte[] bytes) {
private void ParseFrom(byte[] bytes) {
using var stream = new CodedInputStream(bytes);
var ntf = new PlayerStoreNotify();
try {
uint tag;
while ((tag = stream.ReadTag()) != 0) {
@@ -33,16 +34,16 @@ public class PlayerStoreNotify {
case 0: { // is VarInt
var value = stream.ReadUInt32();
if (value < 10) {
ntf.StoreType = (StoreType) value;
StoreType = (StoreType) value;
} else {
ntf.WeightLimit = value;
WeightLimit = value;
}
continue;
}
case 2: { // is LengthDelimited
using var eStream = stream.ReadLengthDelimitedAsStream();
while (eStream.PeekTag() != 0) {
ntf.ItemList.Add(Item.Parser.ParseFrom(eStream));
ItemList.Add(Item.Parser.ParseFrom(eStream));
}
break;
}
@@ -54,7 +55,6 @@ public class PlayerStoreNotify {
File.WriteAllBytes("store_raw_data.bin", bytes);
Environment.Exit(0);
}
return ntf;
}
}

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json;
using YaeAchievement;
using YaeAchievement.Parsers;
using YaeAchievement.res;
@@ -39,7 +40,17 @@ if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null)
}
}
StartAndWaitResult(AppConfig.GamePath, new Dictionary<byte, Func<byte[], bool>> {
StartAndWaitResult(AppConfig.GamePath, new Dictionary<byte, Func<BinaryReader, bool>> {
{ 1, AchievementAllDataNotify.OnReceive },
{ 2, PlayerStoreNotify.OnReceive }
{ 2, PlayerStoreNotify.OnReceive },
{ 100, PlayerPropNotify.OnReceive },
}, () => {
#if DEBUG
PlayerPropNotify.OnFinish();
File.WriteAllText("store_data.json", JsonSerializer.Serialize(PlayerStoreNotify.Instance, new JsonSerializerOptions {
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
}));
#endif
AchievementAllDataNotify.OnFinish();
});

View File

@@ -5,7 +5,6 @@ using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
@@ -110,10 +109,7 @@ public static class Utils {
Environment.Exit(0);
}
}
if (useLocalLib) {
Console.WriteLine(@"[DEBUG] Use local native lib.");
File.Copy(Path.Combine(GlobalVars.AppPath, "YaeAchievementLib.dll"), GlobalVars.LibFilePath, true);
} else if (info.EnableLibDownload) {
if (info.EnableLibDownload && !useLocalLib) {
var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data);
}
@@ -207,17 +203,14 @@ public static class Utils {
};
}
private static bool _isUnexpectedExit;
// ReSharper disable once UnusedMethodReturnValue.Global
public static Thread StartAndWaitResult(string exePath, Dictionary<byte, Func<byte[], bool>> handlers) {
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
try {
File.Delete(GlobalVars.LibFilePath);
} catch (Exception) { /* ignored */ }
};
public static Thread StartAndWaitResult(string exePath, Dictionary<byte, Func<BinaryReader, bool>> handlers, Action onFinish) {
if (!Injector.CreateProcess(exePath, out var hProcess, out var hThread, out var pid)) {
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("ICreateProcess fail"));
}
if (Injector.LoadLibraryAndInject(hProcess,Encoding.UTF8.GetBytes(GlobalVars.LibFilePath)) != 0)
if (Injector.LoadLibraryAndInject(hProcess,GlobalVars.LibFilePath.AsSpan()) != 0)
{
if (!Native.TerminateProcess(hProcess, 0))
{
@@ -228,7 +221,7 @@ public static class Utils {
proc = Process.GetProcessById(Convert.ToInt32(pid));
proc.EnableRaisingEvents = true;
proc.Exited += (_, _) => {
if (handlers.Count != 0)
if (_isUnexpectedExit)
{
proc = null;
Console.WriteLine(App.GameProcessExit);
@@ -255,10 +248,15 @@ public static class Utils {
using var reader = new BinaryReader(server);
while (!proc.HasExited) {
var type = reader.ReadByte();
var length = reader.ReadInt32(); // huh
var data = reader.ReadBytes(length);
if (handlers.Remove(type, out var handler)) {
handler(data);
if (type == 0xFF) {
_isUnexpectedExit = false;
onFinish();
break;
}
if (handlers.TryGetValue(type, out var handler)) {
if (handler(reader)) {
handlers.Remove(type);
}
}
}
});

View File

@@ -62,13 +62,14 @@
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>NotSet</SubSystem>
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net6.0</Command>
<Command>copy $(TargetPath) C:\ProgramData\Yae\YaeAchievement.dll /y</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -92,7 +93,7 @@
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
</Link>
<PostBuildEvent>
<Command>copy $(TargetPath) $(ProjectDir)..\bin\Debug\net8.0-windows\win-x64\YaeAchievementLib.dll /y</Command>
<Command>copy $(TargetPath) C:\ProgramData\Yae\YaeAchievement.dll /y</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>

View File

@@ -12,7 +12,7 @@
#include "ntprivate.h"
CRITICAL_SECTION CriticalSection;
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index = 0);
void SetBreakpoint(HANDLE thread, uintptr_t address, bool enable, uint8_t index);
namespace
{
@@ -43,9 +43,9 @@ namespace Hook {
const auto ToUInt16 = reinterpret_cast<decltype(&BitConverter_ToUInt16)>(Offset.BitConverter_ToUInt16);
EnterCriticalSection(&CriticalSection);
SetBreakpoint((HANDLE)-2, 0, false);
SetBreakpoint((HANDLE)-2, 0, false, 0);
const auto ret = ToUInt16(val, startIndex);
SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true);
SetBreakpoint((HANDLE)-2, Offset.BitConverter_ToUInt16, true, 0);
LeaveCriticalSection(&CriticalSection);
if (ret != 0xAB89)
@@ -78,8 +78,10 @@ namespace Hook {
if (!PlayerStoreWritten)
PlayerStoreWritten = packetType == PacketType::Inventory;
if (AchievementsWritten && PlayerStoreWritten)
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
{
if (!MessagePipe.Write(PacketType::End))
Util::Win32ErrorDialog(9001, GetLastError());
#ifdef _DEBUG
system("pause");
#endif
@@ -88,6 +90,44 @@ namespace Hook {
return ret;
}
void __fastcall AccountDataItem_UpdateNormalProp(const void* __this, const int type, const double value, const double lastValue, const int state)
{
using namespace Globals;
const auto UpdateNormalProp = reinterpret_cast<decltype(&AccountDataItem_UpdateNormalProp)>(Offset.AccountDataItem_UpdateNormalProp);
EnterCriticalSection(&CriticalSection);
SetBreakpoint((HANDLE)-2, 0, false, 1);
UpdateNormalProp(__this, type, value, lastValue, state);
SetBreakpoint((HANDLE)-2, Offset.AccountDataItem_UpdateNormalProp, true, 1);
LeaveCriticalSection(&CriticalSection);
#ifdef _DEBUG
std::println("PropType: {}", type);
std::println("PropState: {}", state);
std::println("PropValue: {}", value);
std::println("PropLastValue: {}", lastValue);
#endif
if (RequiredPlayerProperties.erase(type) != 0)
{
if (!MessagePipe.Write(PacketType::PropData))
Util::Win32ErrorDialog(2002, GetLastError());
if (!MessagePipe.Write(type))
Util::Win32ErrorDialog(2003, GetLastError());
if (!MessagePipe.Write(value))
Util::Win32ErrorDialog(2004, GetLastError());
}
if (AchievementsWritten && PlayerStoreWritten && RequiredPlayerProperties.size() == 0)
{
if (!MessagePipe.Write(PacketType::End))
Util::Win32ErrorDialog(9001, GetLastError());
#ifdef _DEBUG
system("pause");
#endif
ExitProcess(0);
}
}
}
LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
@@ -98,13 +138,17 @@ LONG __stdcall VectoredExceptionHandler(PEXCEPTION_POINTERS ep)
if (exceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
if (exceptionRecord->ExceptionAddress != reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
return EXCEPTION_CONTINUE_SEARCH;
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.BitConverter_ToUInt16)) {
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::BitConverter_ToUInt16);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
if (exceptionRecord->ExceptionAddress == reinterpret_cast<void*>(Offset.AccountDataItem_UpdateNormalProp)) {
contextRecord->Rip = reinterpret_cast<DWORD64>(Hook::AccountDataItem_UpdateNormalProp);
contextRecord->EFlags &= ~0x100; // clear the trap flag
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_CONTINUE_SEARCH;
@@ -178,7 +222,8 @@ DWORD __stdcall ThreadProc(LPVOID hInstance)
if (const auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID))
{
EnterCriticalSection(&CriticalSection);
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true);
SetBreakpoint(hThread, Offset.BitConverter_ToUInt16, true, 0);
SetBreakpoint(hThread, Offset.AccountDataItem_UpdateNormalProp, true, 1);
CloseHandle(hThread);
LeaveCriticalSection(&CriticalSection);
}

View File

@@ -27,11 +27,27 @@ namespace Globals
inline bool AchievementsWritten = false;
inline bool PlayerStoreWritten = false;
/*
* PROP_PLAYER_HCOIN = 10015,
* PROP_PLAYER_WAIT_SUB_HCOIN = 10022,
* PROP_PLAYER_SCOIN = 10016,
* PROP_PLAYER_WAIT_SUB_SCOIN = 10023,
* PROP_PLAYER_MCOIN = 10025,
* PROP_PLAYER_WAIT_SUB_MCOIN = 10026,
* PROP_PLAYER_HOME_COIN = 10042,
* PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043,
* PROP_PLAYER_ROLE_COMBAT_COIN = 10053,
* PROP_PLAYER_MUSIC_GAME_BOOK_COIN = 10058,
*/
inline std::unordered_set<int> RequiredPlayerProperties = { 10015, 10022, 10016, 10023, 10025, 10026, 10042, 10043, 10053, 10058 };
class Offsets
{
public:
PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0, 0);
//PROPERTY2(uintptr_t, BitConverter_ToUInt16, 0x0F826CF0, 0x0F825F10); // use non-zero to override dynamic search
PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0, 0);
//PROPERTY2(uintptr_t, AccountDataItem_UpdateNormalProp, 0x0D9FE060, 0x0D94D910); // use non-zero to override dynamic search
};
inline Offsets Offset;

View File

@@ -514,6 +514,46 @@ namespace
std::println("PlayerStoreId: {}", Globals::PlayerStoreId);
}
void Resolve_AccountDataItem_UpdateNormalProp()
{
if (Globals::Offset.AccountDataItem_UpdateNormalProp != 0) {
Globals::Offset.AccountDataItem_UpdateNormalProp += Globals::BaseAddress;
return;
}
const auto il2cppSection = GetSection("il2cpp");
/*
add ??, 0FFFFD8EEh
cmp ??, 30h
*/
auto candidates = Util::PatternScanAll(il2cppSection, "81 ? EE D8 FF FF ? 83 ? 30");
// should have only one result
if (candidates.size() != 1)
{
std::println("Filtered Instructions: {}", candidates.size());
return;
}
auto fp = candidates[0];
const auto isFunctionEntry = [](uintptr_t va) -> bool {
auto* code = reinterpret_cast<uint8_t*>(va);
/* push rsi */
/* push rdi */
return (va % 16 == 0 && code[0] == 0x56 && code[1] == 0x57);
};
auto range = std::views::iota(0, 213);
if (const auto it = std::ranges::find_if(range, [&](int i) { return isFunctionEntry(fp - i); }); it != range.end()) {
fp -= *it;
} else {
std::println("Failed to find function entry");
return;
}
Globals::Offset.AccountDataItem_UpdateNormalProp = fp;
}
}
bool InitIL2CPP()
@@ -537,14 +577,17 @@ bool InitIL2CPP()
std::future<void> resolveFuncFuture = std::async(std::launch::async, Resolve_BitConverter_ToUInt16);
std::future<void> resolveCmdIdFuture = std::async(std::launch::async, ResolveAchivementCmdId);
std::future<void> resolveInventoryFuture = std::async(std::launch::async, ResolveInventoryCmdId);
std::future<void> resolveUpdatePropFuture = std::async(std::launch::async, Resolve_AccountDataItem_UpdateNormalProp);
resolveFuncFuture.get();
resolveCmdIdFuture.get();
resolveInventoryFuture.get();
resolveUpdatePropFuture.get();
std::println("BaseAddress: 0x{:X}", BaseAddress);
std::println("IsCNREL: {:d}", IsCNREL);
std::println("BitConverter_ToUInt16: 0x{:X}", Offset.BitConverter_ToUInt16);
std::println("AccountDataItem_UpdateNormalProp: 0x{:X}", Offset.AccountDataItem_UpdateNormalProp);
if (!AchievementId && AchievementIdSet.empty())
{

View File

@@ -11,6 +11,8 @@ enum class PacketType : uint8_t
None = 0,
Achivement = 1,
Inventory = 2,
PropData = 100,
End = 255,
};
template <typename T>