[Game] 2d游戏透视实现
chd透视实现
简介
这是一篇简单的d3d游戏内部透视的实现过程,实现的是2d游戏的透视效果,同理可以实现3d fps游戏的透视,绘制思路差不多,只是不是找坐标而是找矩阵,得到屏幕系和世界坐标系的变换矩阵,不多赘述。
本文实现了对未在屏幕内的物体的定位显示,帮助玩家判断地图位置和怪物位置。
基本思路
找到世界系下人物坐标和传送门坐标,两差值即可得到传送门相对人物坐标系的坐标,再找到屏幕坐标系下人物坐标,即可利用相对坐标绘制目标位置。
绘制上,利用inline hook,hook d3d的endscene函数,跳转到坐标内存读取的函数,绘制结果,最后恢复hook。
此外,虽然可以不hook,不inject就可以在全局上开一个新进程进行绘制,但是游戏绘制的窗口在所有窗口的最前面,会挡着其他程序的显示,所以后面就用inline hook改进成了内部绘制。
找坐标
世界系下的坐标可以利用小地图的数值进行查找,浮点数,根据CE的访问地址可以一步一步得到基址。
分析得到:
世界系下,
传送门x = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC]
传送门y = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC+ 04]
自己x = [[[[LaTaleClient.exe+565254] + 7C] + F4] + 08]
自己y = [[[[LaTaleClient.exe+565254] + 7C] + F4] + 08 + 04]
屏幕系下的坐标可以利用QQ截图功能大致估计一个结果,浮点搜索即可:
屏幕系下,
传送门x = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC]
传送门y = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC + 04]
自己x = [[[[LaTaleClient.exe+565254] + 7C] + 104] + 32C]
自己y = [[[[LaTaleClient.exe+565254] + 7C] + 104] + 32C + 04]
此外,可以用类似方法,在怪物走到屏幕边缘的时候,得到地图内怪物的坐标:
世界系下,
怪物x = [[[[[LaTaleClient.exe+565254] + 24] - 4 * i] + 68] + 08]
怪物y = [[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + 08 + 04]
怪物标志位 = [[[[[LaTaleClient.exe+565254] + 24] - 4 * i] + 68] + 04 + 18]
通过怪物标志位判断是否是怪物,为0表示非怪物单位。
绘制
拿到所有坐标基址后,考虑如何绘制。
一个简单的思路是新起一个单独的进程,创建一个凌驾于游戏窗口之上的透明窗体,在透明窗体内进行绘制,缺点是该窗体会在其他窗体上方,会遮挡其他窗体,优点就是写起来特别简单。。
最开始是实现了这一版的,后来觉得实在不妥,就写了dll版的内部绘制。
主要思路是,hook d3d的endscene函数,执行绘制函数,恢复之。
HRESULT __stdcall GUI::EndScene_(IDirect3DDevice9* direct3ddevice9) {
static bool is_first_call_ = true;
if (is_first_call_) {
is_first_call_ = false;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGuiIO& io = ImGui::GetIO();
(void)io;
io.IniFilename = nullptr;
io.LogFilename = nullptr;
io.Fonts->AddFontFromFileTTF(
"C:/Users/Administrator/AppData/Local/Microsoft/Windows/Fonts/"
"1640582872194027.otf",
14.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull());
ImGui_ImplWin32_Init(FindWindow(NULL, "LaTale Client"));
ImGui_ImplDX9_Init(direct3ddevice9);
prev_func_ = (WNDPROC)SetWindowLongA(FindWindow(NULL, "LaTale Client"),
GWL_WNDPROC, (ULL)ProcFunc);
}
hook_Endcene_->Restore();
ImGui_ImplDX9_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGui::Begin(u8"彩虹ar", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove);
ImGui::SetWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::Text(u8"彩虹ar");
ImGui::Checkbox(u8"传送门透视", &EnableShow);
ImGui::End();
auto draw_list_ptr = ImGui::GetBackgroundDrawList();
gamer_->SetDrawList(draw_list_ptr);
if (EnableShow) {
gamer_->Run();
}
ImGui::EndFrame();
ImGui::Render();
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
HRESULT result = direct3ddevice9->EndScene();
hook_Endcene_->Modify();
return result;
}
Hook
之所以可以hook d3d的endscene函数,是因为IDirect3DDevice9类的函数是虚函数,因此一定会在类对象的第一个字节维护一个虚函数表的地址,我们获取虚函数表,根据虚函数顺序即可得到该函数在表里的位置,从而得到函数地址。拿到函数地址之后,将函数的前几个字节改为jmp指令,跳转到我们的内存读取和绘图的函数,然后跳转回去执行原来的函数。
例如,IDirect3DDevice9类的虚函数有下列顺序,因此如果要hook Reset函数就可以修改虚函数表里下标为16的函数指令。
我们hook修改的指令长度是5个字节,因为是32位的游戏,地址长度是4字节,指令占1字节。我们将函数的前5个字节修改为jmp xxx,及无条件跳转到xxx地址,jmp的参数是地址的offset,所以后四个字节对应的数据就是:
offset = target_address - ori_address - 5
code中汇编的jmp对应的字节码为:\xe9,故这么写。
短跳转(Short Jmp,只能跳转到256字节的范围内),对应机器码:EB
近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9
远跳转(Far Jmp,可跳至任意地址),对应机器码: EA
此外,修改指令前需要先修改内存位置的读写属性,确保可以修改。对应VirtualProtect。
hook.h
class Hooker {
using Ptr = std::shared_ptr<Hooker>;
static const int INST_LEN = 5;
private:
u_char inst_ori_[INST_LEN];
u_char inst_tgt_[INST_LEN];
ULL addr_ori_ = 0;
ULL addr_tgt_ = 0;
DWORD ModAttr(const ULL& addr, DWORD attr = PAGE_EXECUTE_READWRITE);
public:
Hooker() = default;
~Hooker() = default;
virtual Ptr Create();
virtual bool Init(const ULL& addr_ori, const ULL& addr_tgt);
virtual bool Modify();
virtual bool Restore();
};
hook.cpp
#include "hook.h"
Hooker::Ptr Hooker::Create() { return std::make_shared<Hooker>(); }
bool Hooker::Init(const ULL& addr_ori, const ULL& addr_tgt) {
addr_ori_ = addr_ori;
addr_tgt_ = addr_tgt;
inst_tgt_[0] = '\xe9';
int offset = addr_tgt_ - (addr_ori_ + INST_LEN);
memcpy(&inst_tgt_[1], &offset, INST_LEN - 1); // byte copy
DWORD attr = ModAttr(addr_ori_);
memcpy(inst_ori_, reinterpret_cast<void*>(addr_ori_), INST_LEN);
return ModAttr(addr_ori_, attr);
}
DWORD Hooker::ModAttr(const ULL& addr, DWORD attr) {
DWORD old_attr;
VirtualProtect(reinterpret_cast<void*>(addr), INST_LEN, attr, &old_attr);
return old_attr;
}
bool Hooker::Modify() {
DWORD attr = ModAttr(addr_ori_);
memcpy(reinterpret_cast<void*>(addr_ori_), inst_tgt_, INST_LEN);
ModAttr(addr_ori_, attr);
return true;
}
bool Hooker::Restore() {
DWORD attributes = ModAttr(addr_ori_);
memcpy(reinterpret_cast<void*>(addr_ori_), inst_ori_, INST_LEN);
ModAttr(addr_ori_, attributes);
return true;
}
因此hook这里可以简单实现为:
ULL* direct3d9_table = (ULL*)*(ULL*)i_direct3d9;
ULL* direct3ddevice9_table = (ULL*)*(ULL*)i_direct3ddevice9;
hook_Reset_->Init(direct3ddevice9_table[16], ULL(Reset_));
hook_Endcene_->Init(direct3ddevice9_table[42], ULL(EndScene_));
hook_Reset_->Modify();
hook_Endcene_->Modify();
最后,inject可以参考https://bbs.pediy.com/thread-269910.htm#msg_header_h1_6,实现一个简单的注入程序。