Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

[Client Edit] v83 Remove Exp Limit

Skilled Illusionist
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 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)
  • 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::Decode and GW_CharacterStat::DecodeChangeStat, so just find the equivalent Odin names and the packet part should be resolved.
 
Initiate Mage
Joined
May 6, 2022
Messages
4
Reaction score
0
um.. i'm so sad because your client edit template repo link is dead ..would you open that link again?
 
Newbie Spellweaver
Joined
Jul 5, 2016
Messages
23
Reaction score
0
InitExpTable(); // can be found at:
link dead any one can share?
 
Back
Top