修改Teeworlds源码,实现自动瞄准(Aimbot)
Teeworlds是一个开源的网络射击游戏。既然开源,也就意味着他的代码可以修改,并重新编译。那么,实现各种外挂(其实修改源码的方式已经是内挂了)也就成为可能了。 这里先描述一下我的需求。我打游戏没有天赋,顾及移动的时候,就顾不了瞄准了。所以我希望电脑能够帮我自动瞄准。至于瞄准策略,那个是可以根据需求自己决定的。 在技术上,我需要知道哪些代码决定了游戏中我的枪口的方向,然后修改这些代码。当然,我的代码光光能控制枪口方向还是不够的,因为我需要一些基础数据来计算出枪口应该朝哪个方向。 目前我采用最最基本的瞄准策略——始终瞄准距离我最近的敌人。在这个需求中,我就需要获取我自己的坐标和所有敌人的坐标,然后依次计算出距离,找出距离我最近的敌人。有了我自己的坐标和最近的敌人的坐标,那么枪口的方向就可以计算出来了。 OK,开始吧。 ==============阶段一:熟悉编译过程============= 我的编译环境是CentOS 5.5。 首先,下载Teeworlds源码包并解压:#HREF"http://downloads.teeworlds.com/teeworlds-0.6.2-source.tar.gz"#-HREF1teeworlds-0.6.2-source.tar.gz#-HREF2: +++code cd ~ wget http://downloads.teeworlds.com/teeworlds-0.6.2-source.tar.gz tar xvf teeworlds-0.6.2-source.tar.gz ---code 由于Teeworlds的编译不同寻常,他是需要一个叫做bam的工具支持的。所以我们再去下载bam,然后源码编译bam: +++code wget teeworlds.com/files/bam-0.4.0.zip unzip bam-0.4.0.zip cd bam-0.4.0 sh make_unix.sh ---code 片刻等待之后,bam就编译完了。 为了方便接下来的操作,我把bam这个可执行文件复制到teeworlds-0.6.2-source这个目录下: +++code cp bam ../teeworlds-0.6.2-source/bam ---code 现在就可以用bam来源码编译teeworlds了。在这个过程中,需要使用到python和SDL库,python在一般的linux上都有,但是必须要有hashlib这个库,在CentOS 5.5上默认是没有的,需要安装一下(较高版本的Ubuntu默认已经安装就不需了的要安装了): +++code cd ~ yum install python-devel wget https://pypi.python.org/packages/source/h/hashlib/hashlib-20081119.zip --no-check-certificate unzip hashlib-20081119.zip cd hashlib-20081119 python setup.py install ---code 现在来安装一下SDL库(较高版本的Ubuntu默认已经安装就不需了的要安装了): +++code yum install SDL-devel ---code 接下来就可以使用bam来编译了: +++code cd ~/teeworlds-0.6.2-source ./bam client_release ---code 片刻之后就编译完啦!此时有了一个teeworlds可执行文件,运行之试试: +++code ./teeworlds ---code 成功运行!是不是棒棒哒! 1.jpg 为了接下来调试方便,我们需要禁用teeworlds的全屏显示。在Settings ==> Graphics 中取消 fullscreen,并且选择一个适当的分辨率,使得屏幕还有足够的空间可以查看调试信息。 2.jpg 好吧,我不得不承认这篇博客一会儿在CentOS 5.5上写的,一会儿是在Ubuntu 14.04上写的。不过所有命令肯定都能在CentOS 5.5上通过。至于Ubuntu,只是安装各种库的时候使用apt-get,库的名字不同,其他都是通用的。 =========阶段二:得知子弹发射时机=========== 首先,在src/game目录下新建myhook.h和myhook.cpp两个文件,用来编写自己的代码。然后只要把自己的函数插入到原代码的某些位置就好了。 通过研读源代码可以发现(研读的过程当然不容易啦),当有用户操作时,src/game/client/components/binds.cpp中的如下函数会被调用: +++code bool CBinds::OnInput(IInput::CEvent e) { don't handle invalid events and keys that arn't set to anything if(e.m_Key <= 0 || e.m_Key >= KEY_LAST || m_aaKeyBindings[e.m_Key][0] == 0) return false; int Stroke = 0; if(e.m_Flags&IInput::FLAG_PRESS) Stroke = 1; Console()->ExecuteLineStroked(Stroke, m_aaKeyBindings[e.m_Key]); return true; } ---code 那么就把这个函数改为如下: +++code bool CBinds::OnInput(IInput::CEvent e) { don't handle invalid events and keys that arn't set to anything if(e.m_Key <= 0 || e.m_Key >= KEY_LAST || m_aaKeyBindings[e.m_Key][0] == 0) return false; #REDmyhook_on_input(m_aaKeyBindings[e.m_Key]);#-RED int Stroke = 0; if(e.m_Flags&IInput::FLAG_PRESS) Stroke = 1; Console()->ExecuteLineStroked(Stroke, m_aaKeyBindings[e.m_Key]); return true; } ---code 当然,需要在binds.cpp中引入头文件: +++code #include <game/myhook.h> ---code 然后在src/game/myhook.h中加入如下函数定义: +++code #ifndef MYHOOK_H #define MYHOOK_H void myhook_on_input(char* p_operation); #endif ---code 并在src/game/myhook.cpp中加入如下函数实现: +++code #include <stdio.h> void myhook_on_input(char* p_operation) { printf("myhook_on_input('%s')n",p_operation); } ---code 好,重新编译!等等,忘记提醒一点了:teeworlds为了保持版本一致,每次编译时,都会对代码进行一次MD5运算,得出一个校验值,并且植入代码中。每次运行时,客户端都会发送校验码,与服务器上保存的值进行校对。只有匹配,才能进入游戏。而我们修改代码以后,校验码肯定不同了。所以我们需要伪造校验码~~校验码是由scrpits/cmd5.py这个python脚本生成的。 打开scripts/cmd5.py这个文件,不管里面是啥代码,全部清空,写入这么两句就够了: +++code hash = "626fce9a778df4d4" print('#define GAME_NETVERSION_HASH "%s"' % hash) ---code 现在可以编译并进入游戏: +++code cd ~/teeworlds-0.6.2-source/ ./bam client_release ./teeworlds ---code 随便进一局游戏,左右移动、跳、放钩子、射击,看看控制台的输出: 3.jpg 现在我们已经能够得知所有用户操作的时机了!如果要得知子弹发射的时机,只需要判断传入的参数是不是字符串”+fire”即可~~ ===============阶段三:控制子弹发射方向================ 光光知道子弹什么时候发射了还不够,我们还得能够控制它!继续研读代码…… 在src/game/client/components/controls.cpp中,找到如下函数: +++code int CControls::SnapInput(int *pData) { static int64 LastSendTime = 0; bool Send = false; // update player state if(m_pClient->m_pChat->IsActive()) m_InputData.m_PlayerFlags = PLAYERFLAG_CHATTING; else if(m_pClient->m_pMenus->IsActive()) m_InputData.m_PlayerFlags = PLAYERFLAG_IN_MENU; else m_InputData.m_PlayerFlags = PLAYERFLAG_PLAYING; if(m_pClient->m_pScoreboard->Active()) m_InputData.m_PlayerFlags |= PLAYERFLAG_SCOREBOARD; if(m_LastData.m_PlayerFlags != m_InputData.m_PlayerFlags) Send = true; m_LastData.m_PlayerFlags = m_InputData.m_PlayerFlags; // we freeze the input if chat or menu is activated if(!(m_InputData.m_PlayerFlags&PLAYERFLAG_PLAYING)) { OnReset(); mem_copy(pData, &m_InputData, sizeof(m_InputData)); // send once a second just to be sure if(time_get() > LastSendTime + time_freq()) Send = true; } else { m_InputData.m_TargetX = (int)m_MousePos.x; m_InputData.m_TargetY = (int)m_MousePos.y; if(!m_InputData.m_TargetX && !m_InputData.m_TargetY) { m_InputData.m_TargetX = 1; m_MousePos.x = 1; } // set direction m_InputData.m_Direction = 0; if(m_InputDirectionLeft && !m_InputDirectionRight) m_InputData.m_Direction = -1; if(!m_InputDirectionLeft && m_InputDirectionRight) m_InputData.m_Direction = 1; // stress testing if(g_Config.m_DbgStress) { float t = Client()->LocalTime(); mem_zero(&m_InputData, sizeof(m_InputData)); m_InputData.m_Direction = ((int)t/2)&1; m_InputData.m_Jump = ((int)t); m_InputData.m_Fire = ((int)(t*10)); m_InputData.m_Hook = ((int)(t*2))&1; m_InputData.m_WantedWeapon = ((int)t)%NUM_WEAPONS; m_InputData.m_TargetX = (int)(sinf(t*3)*100.0f); m_InputData.m_TargetY = (int)(cosf(t*3)*100.0f); } // check if we need to send input if(m_InputData.m_Direction != m_LastData.m_Direction) Send = true; else if(m_InputData.m_Jump != m_LastData.m_Jump) Send = true; else if(m_InputData.m_Fire != m_LastData.m_Fire) Send = true; else if(m_InputData.m_Hook != m_LastData.m_Hook) Send = true; else if(m_InputData.m_WantedWeapon != m_LastData.m_WantedWeapon) Send = true; else if(m_InputData.m_NextWeapon != m_LastData.m_NextWeapon) Send = true; else if(m_InputData.m_PrevWeapon != m_LastData.m_PrevWeapon) Send = true; // send at at least 10hz if(time_get() > LastSendTime + time_freq()/25) Send = true; } // copy and return size m_LastData = m_InputData; if(!Send) return 0; LastSendTime = time_get(); mem_copy(pData, &m_InputData, sizeof(m_InputData)); return sizeof(m_InputData); } ---code 把它改为: +++code int CControls::SnapInput(int *pData) { static int64 LastSendTime = 0; bool Send = false; // update player state if(m_pClient->m_pChat->IsActive()) m_InputData.m_PlayerFlags = PLAYERFLAG_CHATTING; else if(m_pClient->m_pMenus->IsActive()) m_InputData.m_PlayerFlags = PLAYERFLAG_IN_MENU; else m_InputData.m_PlayerFlags = PLAYERFLAG_PLAYING; if(m_pClient->m_pScoreboard->Active()) m_InputData.m_PlayerFlags |= PLAYERFLAG_SCOREBOARD; if(m_LastData.m_PlayerFlags != m_InputData.m_PlayerFlags) Send = true; m_LastData.m_PlayerFlags = m_InputData.m_PlayerFlags; // we freeze the input if chat or menu is activated if(!(m_InputData.m_PlayerFlags&PLAYERFLAG_PLAYING)) { OnReset(); mem_copy(pData, &m_InputData, sizeof(m_InputData)); // send once a second just to be sure if(time_get() > LastSendTime + time_freq()) Send = true; } else { #RED //m_InputData.m_TargetX = (int)m_MousePos.x; //m_InputData.m_TargetY = (int)m_MousePos.y; myhook_get_target(&m_InputData.m_TargetX,&m_InputData.m_TargetY,(int)m_MousePos.x,(int)m_MousePos.y); #-RED if(!m_InputData.m_TargetX && !m_InputData.m_TargetY) { m_InputData.m_TargetX = 1; m_MousePos.x = 1; } // set direction m_InputData.m_Direction = 0; if(m_InputDirectionLeft && !m_InputDirectionRight) m_InputData.m_Direction = -1; if(!m_InputDirectionLeft && m_InputDirectionRight) m_InputData.m_Direction = 1; // stress testing if(g_Config.m_DbgStress) { float t = Client()->LocalTime(); mem_zero(&m_InputData, sizeof(m_InputData)); m_InputData.m_Direction = ((int)t/2)&1; m_InputData.m_Jump = ((int)t); m_InputData.m_Fire = ((int)(t*10)); m_InputData.m_Hook = ((int)(t*2))&1; m_InputData.m_WantedWeapon = ((int)t)%NUM_WEAPONS; m_InputData.m_TargetX = (int)(sinf(t*3)*100.0f); m_InputData.m_TargetY = (int)(cosf(t*3)*100.0f); } // check if we need to send input if(m_InputData.m_Direction != m_LastData.m_Direction) Send = true; else if(m_InputData.m_Jump != m_LastData.m_Jump) Send = true; else if(m_InputData.m_Fire != m_LastData.m_Fire) Send = true; else if(m_InputData.m_Hook != m_LastData.m_Hook) Send = true; else if(m_InputData.m_WantedWeapon != m_LastData.m_WantedWeapon) Send = true; else if(m_InputData.m_NextWeapon != m_LastData.m_NextWeapon) Send = true; else if(m_InputData.m_PrevWeapon != m_LastData.m_PrevWeapon) Send = true; // send at at least 10hz if(time_get() > LastSendTime + time_freq()/25) Send = true; } // copy and return size m_LastData = m_InputData; if(!Send) return 0; LastSendTime = time_get(); mem_copy(pData, &m_InputData, sizeof(m_InputData)); return sizeof(m_InputData); } ---code 当然,需要在controls.cpp中引入头文件: +++code #include <game/myhook.h> ---code 然后在src/game/myhook.h中加入如下函数定义: +++code void myhook_get_target(int* p_targetx,int* p_targety,int p_mousex,int p_mousey); ---code 并在src/game/myhook.cpp中加入如下函数实现: +++code void myhook_get_target(int* p_targetx,int* p_targety,int p_mousex,int p_mousey) { *p_targetx=1; *p_targety=-1; } ---code 方向(也就是画面上枪口的位置),你发射的子弹和放出的钩子都是朝右上方45度角发射的,即代码中硬编码的(1,-1)向量。 4.jpg 5.jpg 当然,你也可以写任何你希望的角度,方法就是myhook_get_target方法中设置*p_targetx和*p_targety的值。 ===================阶段三:获取所有玩家的信息===================== 离我们的目标越来越近了。在实现我们的算法之前,还需要获得一样东西,那就是能够遍历所有玩家的信息,比如队伍、坐标等等。 还是研读代码咯~ 首先在src/game/client/gameclient.cpp中找到函数: +++code void CGameClient::OnInit() { m_pGraphics = Kernel()->RequestInterface(); // propagate pointers m_UI.SetGraphics(Graphics(), TextRender()); m_RenderTools.m_pGraphics = Graphics(); m_RenderTools.m_pUI = UI(); int64 Start = time_get(); // set the language g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console()); // TODO: this should be different // setup item sizes for(int i = 0; i < NUM_NETOBJTYPES; i++) Client()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i)); // load default font static CFont *pDefaultFont = 0; char aFilename[512]; IOHANDLE File = Storage()->OpenFile("fonts/DejaVuSans.ttf", IOFLAG_READ, IStorage::TYPE_ALL, aFilename, sizeof(aFilename)); if(File) { io_close(File); pDefaultFont = TextRender()->LoadFont(aFilename); TextRender()->SetDefaultFont(pDefaultFont); } if(!pDefaultFont) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "gameclient", "failed to load font. filename='fonts/DejaVuSans.ttf'"); // init all components for(int i = m_All.m_Num-1; i >= 0; --i) m_All.m_paComponents[i]->OnInit(); // setup load amount// load textures for(int i = 0; i < g_pData->m_NumImages; i++) { g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL, CImageInfo::FORMAT_AUTO, 0); g_GameClient.m_pMenus->RenderLoading(); } for(int i = 0; i < m_All.m_Num; i++) m_All.m_paComponents[i]->OnReset(); int64 End = time_get(); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "initialisation finished after %.2fms", ((End-Start)*1000)/(float)time_freq()); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "gameclient", aBuf); m_ServerMode = SERVERMODE_PURE; } ---code 把它改为: +++code void CGameClient::OnInit() { m_pGraphics = Kernel()->RequestInterface(); // propagate pointers m_UI.SetGraphics(Graphics(), TextRender()); m_RenderTools.m_pGraphics = Graphics(); m_RenderTools.m_pUI = UI(); int64 Start = time_get(); // set the language g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console()); // TODO: this should be different // setup item sizes for(int i = 0; i < NUM_NETOBJTYPES; i++) Client()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i)); // load default font static CFont *pDefaultFont = 0; char aFilename[512]; IOHANDLE File = Storage()->OpenFile("fonts/DejaVuSans.ttf", IOFLAG_READ, IStorage::TYPE_ALL, aFilename, sizeof(aFilename)); if(File) { io_close(File); pDefaultFont = TextRender()->LoadFont(aFilename); TextRender()->SetDefaultFont(pDefaultFont); } if(!pDefaultFont) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "gameclient", "failed to load font. filename='fonts/DejaVuSans.ttf'"); // init all components for(int i = m_All.m_Num-1; i >= 0; --i) m_All.m_paComponents[i]->OnInit(); // setup load amount// load textures for(int i = 0; i < g_pData->m_NumImages; i++) { g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL, CImageInfo::FORMAT_AUTO, 0); g_GameClient.m_pMenus->RenderLoading(); } for(int i = 0; i < m_All.m_Num; i++) m_All.m_paComponents[i]->OnReset(); int64 End = time_get(); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "initialisation finished after %.2fms", ((End-Start)*1000)/(float)time_freq()); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "gameclient", aBuf); m_ServerMode = SERVERMODE_PURE; #REDmyhook_on_init(m_aClients,&(m_Snap.m_LocalClientID));#-RED } ---code 当然,需要在gameclient.cpp中引入头文件: +++code #include <game/myhook.h> ---code 然后在src/game/myhook.h中加入如下函数定义: +++code void myhook_on_init(CGameClient::CClientData* p_clients,int* p_myid); ---code 并在src/game/myhook.cpp中加入如下函数实现: +++code #include <game/client/gameclient.h> static CGameClient::CClientData* sg_clients=NULL; static int* sg_myid=NULL; void myhook_on_init(CGameClient::CClientData* p_clients,int* p_myid) { sg_clients=p_clients; sg_myid=p_myid; } ---code 其中sg_clients就是所有玩家数据的数组,而sg_myid就是我的数据在这个数组中的索引。 ====================阶段四:实现最终算法======================== 现在所有需要的数据都能得到手了,而且所有需要的时机都已经掌握了。myhook.h中的三个函数都插入了原代码中。接下来所有的代码只要在myhook.cpp中完成逻辑部分了。 这个不废话了,直接上代码,代码写的很清晰,而且还有注释。 首先是myhook.h: +++code #ifndef MYHOOK_H #define MYHOOK_H #include <game/client/gameclient.h> void myhook_on_init(CGameClient::CClientData* p_clients,int* p_myid); void myhook_on_input(char* p_operation); void myhook_get_target(int* p_targetx,int* p_targety,int p_mousex,int p_mousey); #endif ---code 然后是myhook.cpp: +++code #include <stdio.h> #include <string.h> #include <float.h> #include <game/myhook.h> //所有玩家的数据的数组 static CGameClient::CClientData* sg_clients=NULL; //我的数据在数组中的索引 static int* sg_myid=NULL; //是否正在开火 static bool sg_firing=false; //初始化数据,传入我的坐标的指针和所有玩家的数据的数组 void myhook_on_init(CGameClient::CClientData* p_clients,int* p_myid) { sg_clients=p_clients; sg_myid=p_myid; } //当我有任何操作时被调用,并传入操作码,如+left,+right,+jump,+fire,只检测是否在开火 void myhook_on_input(char* p_operation) { if(strcmp(p_operation,"+fire")==0) sg_firing=true; else sg_firing=false; } //判断是否有两个不同的队伍 static bool is_teams_war() { bool t_has_blue=false; bool t_has_red=false; for(int t_i=0;t_i<MAX_CLIENTS;t_i++) { int t_team=sg_clients[t_i].m_Team; if(t_team==TEAM_BLUE) t_has_blue=true; else if(t_team==TEAM_RED) t_has_red=true; if(t_has_blue&&t_has_red) return true; } return false; } //获取制定索引的玩家的坐标 static inline vec2 get_position(int p_id) { return sg_clients[p_id].m_Predicted.m_Pos; } //计算索引为p_id的角色距离自己的距离的平方 static double get_square_distance(int p_id) { vec2 t_mypos=get_position(*sg_myid); vec2 t_hispos=get_position(p_id); double t_drt_x=t_mypos.x-t_hispos.x; double t_drt_y=t_mypos.y-t_hispos.y; return t_drt_x*t_drt_x+t_drt_y*t_drt_y; } //获取最近的敌人的索引 static int get_closest_enemy() { double t_min_distance=DBL_MAX; int t_closest_enemy=0; if(is_teams_war()) { int t_my_team=sg_clients[*sg_myid].m_Team; for(int t_i=0;t_i<MAX_CLIENTS;t_i++) { int t_team=sg_clients[t_i].m_Team; if(t_team==t_my_team) continue; double t_distance=get_square_distance(t_i); if(t_distance<t_min_distance) { t_min_distance=t_distance; t_closest_enemy=t_i; } } } else { for(int t_i=0;t_i<MAX_CLIENTS;t_i++) { if(t_i==*sg_myid) continue; double t_distance=get_square_distance(t_i); if(t_distance<t_min_distance) { t_min_distance=t_distance; t_closest_enemy=t_i; } } } return t_closest_enemy; } //系统需要获取我的目标是被调用,p_targetx和p_targety就是需要被设置的x,y的指针,而p_mousex和p_mousey是当前鼠标坐标 void myhook_get_target(int* p_targetx,int* p_targety,int p_mousex,int p_mousey) { if(sg_firing) { int t_enemy=get_closest_enemy(); vec2 t_enemy_pos=get_position(t_enemy); vec2 t_my_pos=get_position(*sg_myid); *p_targetx=t_enemy_pos.x-t_my_pos.x; *p_targety=t_enemy_pos.y-t_my_pos.y; } else { *p_targetx=p_mousex; *p_targety=p_mousey; } } ---code 需要注意的一点是,其中判断了游戏的类型,是CTF模式还是DM模式,即,是分组对战还是大混战。 OK,重新编译并进入游戏: +++code cd ~/teeworlds-0.6.2-source/ ./bam client_release ./teeworlds ---code 你会得到惊喜的。发射子弹时,子弹的出射方向始终是最近的敌人,而其他的操作,如左、右、跳、放钩子,都一切正常。 当然,罗嗦一句,子弹是有重力的,轨距会向下弯曲,所以如果要应用于实战中,还需要结合一些物理学知识,来修正一下目标。