Code:
#include <Windows.h>
#include <map>
#include <functional>
#include <iostream>
#include <fstream>
char outputFileName[] = "C:\\Temp\\item_clicked_output.txt";
#define SET_OUTFIT_ADDRESS 0x90840
#define CONTEXT_MENU_ITEM_CLICK_CALLBACK 0x478A6C
// Helper functions
// Thanks to Stepler for HookCall function
void HookCall(DWORD dwCallAddress, DWORD dwNewAddress, LPDWORD pOldAddress)
{
DWORD dwOldProtect, dwNewProtect, dwOldCall, dwNewCall;
BYTE call[5] = { 0xE8, 0x00, 0x00, 0x00, 0x00 };
dwNewCall = dwNewAddress - dwCallAddress - 5;
memcpy(&call[1], &dwNewCall, 4);
HANDLE process = GetCurrentProcess();
VirtualProtectEx(process, (LPVOID)(dwCallAddress), sizeof(call), PAGE_READWRITE, &dwOldProtect);
if (pOldAddress)
{
memcpy(&dwOldCall, (LPVOID)(dwCallAddress + 1), 4);
*pOldAddress = dwCallAddress + dwOldCall + 5;
}
memcpy((LPVOID)(dwCallAddress), &call, sizeof(call));
VirtualProtectEx(process, (LPVOID)(dwCallAddress), sizeof(call), dwOldProtect, &dwNewProtect);
}
// Swaps original callback address with our custom callback address in the vtable
void HookVtableContextMenuItemClickCallback(DWORD dwVtableFunctionAddress, DWORD dwNewAddress, LPDWORD pOldAddress)
{
DWORD dwOldProctect, dwNewProtect;
HANDLE currentProcess = GetCurrentProcess();
VirtualProtectEx(currentProcess, (LPVOID)(dwVtableFunctionAddress), sizeof(DWORD), PAGE_READWRITE, &dwOldProctect);
if (pOldAddress)
{
memcpy(pOldAddress, (LPVOID)(dwVtableFunctionAddress), sizeof(DWORD));
}
memcpy((LPVOID)(dwVtableFunctionAddress), &dwNewAddress, sizeof(DWORD));
VirtualProtectEx(currentProcess, (LPVOID)(dwVtableFunctionAddress), sizeof(DWORD), dwOldProctect, &dwNewProtect);
}
// end of helper functions
// Helper variables
HINSTANCE _dll;
DWORD _tibiaBaseAddress;
// functions prototypes
typedef void(__thiscall* AddContextMenuItem)(void *_this, int eventId, const char *text, const char *shortcut);
typedef void(__thiscall* ContextMenuItemClickCallback)(void *_this, int eventId);
// will hold original Tibia functions;
AddContextMenuItem originalAddContextMenuItem;
ContextMenuItemClickCallback originalContextMenuItemClickCallback;
// ContextMenuItem
class ContextMenuItem
{
public:
ContextMenuItem();
ContextMenuItem(
int eventId,
const std::string &text,
const std::string &shortcut,
bool prependsSeparator,
std::function<void()> callback);
int getEventId() const { return _eventId; }
const char *getText() const { return _text; }
const char *getShortcut() const { return _hasShortcut ? _shortcut : nullptr; }
bool prependsSeparator() const { return _prependsSeparator; }
std::function<void()> getCallback() const { return _callback; }
private:
int _eventId;
char _text[0x32];
char _shortcut[0x14];
bool _hasShortcut;
bool _prependsSeparator;
std::function<void()> _callback;
};
ContextMenuItem::ContextMenuItem()
{
}
ContextMenuItem::ContextMenuItem(
int eventId,
const std::string &text,
const std::string &shortcut,
bool prependsSeparator,
std::function<void()> callback)
{
_eventId = eventId;
int textLen = text.size();
if (textLen == 0)
{
throw std::exception("text cannot be empty");
}
if (textLen >= sizeof(_text))
{
throw std::exception("text is too long");
}
text.copy(_text, textLen, 0);
_text[textLen] = '\0';
_hasShortcut = false;
int shortcutLen = shortcut.size();
if (shortcutLen == 0)
{
_shortcut[0] = '\0';
}
else
{
if (shortcutLen >= sizeof(_shortcut))
{
throw std::exception("shortcut is too long");
}
shortcut.copy(_shortcut, shortcutLen, 0);
_shortcut[shortcutLen] = '\0';
_hasShortcut = true;
}
_prependsSeparator = prependsSeparator;
_callback = callback;
}
// will hold our custom context menus
std::map<int, ContextMenuItem> customContextMenus;
void __fastcall customAddContextMenuItem(void *_this, void *_edx, int eventId, const char *text, const char *shortcut)
{
// We simply don't use the param 'void *_edx'
// store a pointer to prependSeparator
PBYTE prependSeparatorPtr = ((PBYTE)_this) + 0x30;
// save initial prependSeparator state
BYTE prependSeparator = *prependSeparatorPtr;
for (auto keyValuePair = customContextMenus.begin(); keyValuePair != customContextMenus.end(); keyValuePair++)
{
auto current = keyValuePair->second;
// must be set before the function call to prepend a separator
*prependSeparatorPtr = current.prependsSeparator() ? 1 : 0;
// call the original function
originalAddContextMenuItem(_this, current.getEventId(), current.getText(), current.getShortcut());
}
// restore initial prependSeparator state
*prependSeparatorPtr = prependSeparator;
// appends the hooked context menu item
originalAddContextMenuItem(_this, eventId, text, shortcut);
}
void __fastcall customContextMenuItemClickCallback(void *_this, void *_edx, int eventId)
{
// We simply don't use the param 'void *_edx'
auto keyValuePair = customContextMenus.find(eventId);
// if not found
if (keyValuePair == customContextMenus.end())
{
// forward to the original callback function
originalContextMenuItemClickCallback(_this, eventId);
}
else
{
// call our own item-defined callback and don't forward to the original callback function
auto item = keyValuePair->second;
item.getCallback()();
}
}
extern "C" __declspec(dllexport)
void Execute()
{
ContextMenuItem firstItem(1, "Foo", "", true, [&]() -> void
{
std::ofstream file;
file.open(outputFileName, std::ios::out | std::ios::app);
file << "You clicked the Foo item!" << std::endl;
file.close();
});
ContextMenuItem secondItem(2, "Bar", "Ctrl+HUEUHE", false, [&]() -> void
{
std::ofstream file;
file.open(outputFileName, std::ios::out | std::ios::app);
file << "You clicked the Bar item!" << std::endl;
file.close();
});
customContextMenus.clear();
customContextMenus[firstItem.getEventId()] = firstItem;
customContextMenus[secondItem.getEventId()] = secondItem;
HookCall(
_tibiaBaseAddress + SET_OUTFIT_ADDRESS,
(DWORD)(&customAddContextMenuItem),
(LPDWORD)(&originalAddContextMenuItem));
HookVtableContextMenuItemClickCallback(
_tibiaBaseAddress + CONTEXT_MENU_ITEM_CLICK_CALLBACK,
(DWORD)(&customContextMenuItemClickCallback),
(LPDWORD)(&originalContextMenuItemClickCallback));
}
BOOL WINAPI DllMain(
_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved
)
{
_dll = hinstDLL;
_tibiaBaseAddress = (DWORD)GetModuleHandle(NULL);
return TRUE;
}