修改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
你会得到惊喜的。发射子弹时,子弹的出射方向始终是最近的敌人,而其他的操作,如左、右、跳、放钩子,都一切正常。
当然,罗嗦一句,子弹是有重力的,轨距会向下弯曲,所以如果要应用于实战中,还需要结合一些物理学知识,来修正一下目标。