- Joined
- Feb 18, 2010
- Messages
- 320
- Reaction score
- 112
Hello frens,
Had this idea a while ago but never got around to doing it until the other day.
This client-addon will enable you to (from the players perspective) transform the 32-bit integer experience cap to any datatype you want (64-bit int in this example). The way I accomplished this was to store my own experience variable and then, with a couple different methods, hijack all the spots that display the experience string. So yeah, I'm not really changing the datatype, but the end result is you can have more than 2.14b experience required to advance to the next level.
Of all the character stats that are worth upgrading datatype for, exp was the easiest.
Do not ask for other versions, this is more than enough to show you how to do it on pretty much any version.
Feel free to subscribe to
Note: this has not been thoroughly tested but as far as I could tell it works fine.
Requirements:
Credits:
Alright, so to start off, put these guys in a header and class file, respectively:
Then go ahead and put this in another header:
Add this somewhere and call it after you inject the dll.
Here are the two required typedefs.
Here are all the hooks I use:
And finally, the new get_next_level_exp function:
Finally, you'll need to edit your server to store and send 64-bit integer exp values. You're on your own with this part. I only swapped two packet decodes client-side though, and they exist in CharacterData:ecode and GW_CharacterStat:ecodeChangeStat, so just find the equivalent Odin names and the packet part should be resolved.
Had this idea a while ago but never got around to doing it until the other day.
This client-addon will enable you to (from the players perspective) transform the 32-bit integer experience cap to any datatype you want (64-bit int in this example). The way I accomplished this was to store my own experience variable and then, with a couple different methods, hijack all the spots that display the experience string. So yeah, I'm not really changing the datatype, but the end result is you can have more than 2.14b experience required to advance to the next level.
Of all the character stats that are worth upgrading datatype for, exp was the easiest.
Do not ask for other versions, this is more than enough to show you how to do it on pretty much any version.
Feel free to subscribe to
You must be registered to see links
if you appreciate free, leechable content. Next week I'm gonna release my locally renowned potato salad recipe.Note: this has not been thoroughly tested but as far as I could tell it works fine.
Requirements:
- Some knowledge of C/C++ (less if using the repo below)
-
You must be registered to see links
- IDA if you want to port this to other versions
- The ability to read
Credits:
- Zain, Artcro, and their v83 server that I tested on
- All the people that helped me learn poop
- All the people that helped them learn poop
- Etc..
Alright, so to start off, put these guys in a header and class file, respectively:
Code:
#pragma once
#include <Windows.h>
#include <climits> // windows macro library
/*
character data extension class. stores exp for now.
will eventually store meso and maybe level if im feeling like i dont want a life for a month
*/
struct CharacterDataEx
{
private:
static CharacterDataEx* m_pInstance;
public:
LONGLONG m_liExp;
CharacterDataEx()
{
/* setting default value as proof of concept. can be removed. */
m_liExp = 2147483647LL + 1000000000LL;
}
BYTE GetCharLevel();
static CharacterDataEx* GetInstance()
{
if (!m_pInstance)
{
m_pInstance = new CharacterDataEx();
}
return m_pInstance;
}
};
Code:
#include "pch.h"
#include "CharacterDataEx.h"
#include "CWvsContext.h"
#include <climits>
CharacterDataEx* CharacterDataEx::m_pInstance;
BYTE CharacterDataEx::GetCharLevel()
{
// TODO implement https://github.com/67-6f-64/Firefly/blob/master/Firefly%20Spy/ZtlSecure.hpp
auto CUserLocal__GetCharacterLevel = (BYTE(__fastcall*)(PVOID pThis, PVOID edx))0x00949B15;
PVOID CUserLocal__ms_pInstance = *reinterpret_cast<void**>(0x00BEBF98);
return CUserLocal__GetCharacterLevel(CUserLocal__ms_pInstance, NULL);
}
INT CharacterDataEx_C::GetCharMesoInt()
{
return CharacterDataEx::GetInstance()->m_ullMeso > INT_MAX ? INT_MAX : CharacterDataEx::GetInstance()->m_ullMeso;
}
Then go ahead and put this in another header:
Code:
#pragma once
#include "ZArray.h"
#include <Windows.h>
struct CInPacket
{
INT m_bLoopback;
INT m_nState;
ZArray<UCHAR> m_aRecvBuff;
USHORT m_uLength;
USHORT m_uRawSeq;
USHORT m_uDataLen;
UINT m_uOffset;
template<typename T>
T Decode()
{
// TODO write real decode template instead of relying on decodebuffer
typedef INT(__fastcall* _DecodeBuffer_t)(CInPacket* pThis, PVOID edx, PVOID p, size_t nLen);
static _DecodeBuffer_t _DecodeBuffer = reinterpret_cast<_DecodeBuffer_t>(0x00432257);
T retval;
_DecodeBuffer(this, NULL, &retval, sizeof(T));
return retval;
}
};
Add this somewhere and call it after you inject the dll.
Code:
VOID InitExpOverride()
{
InitExpTable(); // can be found at: https://gist.github.com/MinimumDelta/f41bd1787cede7866536e1a6fff68a92
/* GW_CharacterStat::DecodeChangeStat -> hijack decode4 call and switch to decode8, then return int value */
PatchCall(0x004E31B6, ExpSwap__Decode4To8);
/* GW_CharacterStat::Decode -> hijack decode4 call and switch to decode8, then return int value */
PatchCall(0x004E2C6E, ExpSwap__Decode4To8);
_ZXString_char__Assign = reinterpret_cast<_ZXString_char__Assign_t>(0x00414617);
/* CWvsContext::OnStatChanged -> jmping over a segment that looks at exp and then makes pet talk if at a certain % -> cbf fixing this */
WriteValue<BYTE>(0x00A20116, 0xEB);
/* CUIStat::OnMouseMove -> hijack displayed exp in tooltip when hovering in stat window */
PatchCall(0x008C539D, FormatExpString_Hook);
/* CUIStat::Draw -> hijack displayed exp in stat window */
PatchCall(0x008C602E, ZXString__GetConstCharString);
/* CUIStatusBar::ProcessToolTip -> hijack displayed exp in tooltip when hovering exp gauge in stat bar */
PatchCall(0x008D78E3, FormatExpString_Hook);
PatchCall(0x008D789F, FormatExpString_Hook);
/* CUIStatusBar::SetNumberValue -> hijack displayed exp above exp gauge */
WriteValue<BYTE>(0x008DA406 + 1, 64); // increase string size allocation -- v207 = alloca(32)
PatchCall(0x008DA418, itoa_ExpSwap);
/* CUIStatusBar::SetNumberValue -> change arguments so we can override exp gauge */
INITMAPLEHOOK(
_CUIStatusBar__SetNumberValue,
_CUIStatusBar__SetNumberValue_t,
CUIStatusBar__SetNumberValue_Hook,
0x008D850B
);
}
Here are the two required typedefs.
Code:
typedef void(__fastcall* _ZXString_char__Assign_t)(ZXString<char>* pThis, PVOID edx, const char* s, int n);
_ZXString_char__Assign_t _ZXString_char__Assign;
typedef VOID(__fastcall* _CUIStatusBar__SetNumberValue_t)(PVOID pThis, PVOID edx, int hp, int hpMax, int mp, int mpMax, int exp, int expMax, int tempExp);
_CUIStatusBar__SetNumberValue_t _CUIStatusBar__SetNumberValue;
Here are all the hooks I use:
Code:
const char* __fastcall ZXString__GetConstCharString(ZXString<char>* pThis, PVOID edx)
{
std::string s = std::to_string(CharacterDataEx::GetInstance()->m_liExp); // need to include string lib
_ZXString_char__Assign(pThis, NULL, s.c_str(), s.length());
return pThis->m_pStr;
}
// https://www.cplusplus.com/reference/cstdlib/itoa/
PCHAR __cdecl itoa_ExpSwap(int value, PCHAR buffer, int radix)
{
_i64toa(CharacterDataEx::GetInstance()->m_liExp, buffer, radix);
// TODO abbreviate large numbers to something like 14.123B or something -- maybe make toggleable through some UI setting??
return buffer;
}
/* all arguments passed on the stack despite being a member function */
VOID __cdecl FormatExpString_Hook(ZXString<char>* pThis, const char* originalstring, int curexp, int nextlevelexp)
{
std::string s = std::to_string(CharacterDataEx::GetInstance()->m_liExp);
s.append(" / ");
s.append(std::to_string(get_next_level_exp()));
_ZXString_char__Assign(pThis, NULL, s.c_str(), s.length());
}
VOID __fastcall CUIStatusBar__SetNumberValue_Hook(PVOID pThis, PVOID edx, int hp, int hpMax, int mp, int mpMax, int exp, int expMax, int tempExp)
{
LONGLONG liExp = CharacterDataEx::GetInstance()->m_liExp;
LONGLONG liExpMax = get_next_level_exp();
/* this adjusts the exp bar gauge -- idk how else to do this lmao, we're essentially scaling the exp down until itll fit in the data type */
while (liExpMax > INT_MAX || liExp > INT_MAX)
{
liExp >>= 2;
liExpMax >>= 2;
}
exp = liExp;
expMax = liExpMax;
_CUIStatusBar__SetNumberValue(pThis, edx, hp, hpMax, mp, mpMax, exp, expMax, tempExp);
}
INT __fastcall ExpSwap__Decode4To8(CInPacket* pThis, PVOID edx)
{
LONGLONG liExp = pThis->Decode<LONGLONG>();
CharacterDataEx::GetInstance()->m_liExp = liExp;
return liExp < INT_MAX ? (INT)liExp : INT_MAX;
}
And finally, the new get_next_level_exp function:
Code:
/* we cant hook the original function because we are changing the return type */
LONGLONG get_next_level_exp()
{
BYTE lvl = CharacterDataEx::GetInstance()->GetCharLevel();
if (lvl >= sizeof(s_nextLevel) / sizeof(s_nextLevel[0])) return 0;
return s_nextLevel[lvl];
}
Finally, you'll need to edit your server to store and send 64-bit integer exp values. You're on your own with this part. I only swapped two packet decodes client-side though, and they exist in CharacterData:ecode and GW_CharacterStat:ecodeChangeStat, so just find the equivalent Odin names and the packet part should be resolved.