Trickjump bot for Wolf:ET
Wolf:ET inspired me to want to learn how to code especially in C++. Seeing all the cool things people were creating made me want to join in. And I eventually did. Out of all the bots released, a trickjump bot never was... and I really wanted one.
The overall design is inspired by pretty much all the code I've seen. Inject .dll and hook to gain access to the juicy stuff. I will be combining two projects in this write up, the later project lacked key a lot of features but was pretty tiny ~500 lines of code.
Possibly the most annoying thing to do was every update I had to hop back into IDA and track down a couple of offsets so my bot would continue to work. And so the first thing in simplifying my code was to dynamically locate the offset for the last remaining function I needed hooked. The functions bDataCompare and dwFindPattern solved this for me, I also didn't write them. Luckily most of the offsets I needed were for the 2.6b executable, and so they would never change.
Since I played on a server accross the world, my screen would always pop up with disconnected status even though there was no lag, a simple return statement on that pesky DrawDisconnect(void) function saved me from going insane.
There were a few other things like autojump, no hitback when attacted, run while scoped, but it also logged a bunch of stuff on how far off perfect you were jumping, speeds etc.
Eventually I also created a little chat program so I could automatically translate the ingame chat, and see list of players whilst having the game minimised. Heres the code. This was also the first time I had C++ talking to a C# application.
//processing chat everytime something is sent
case CG_GETSERVERCOMMAND: //processing chat and then sends to logger
{
int ret = orig_syscall(command, arg[0]);
if (!arg[0])
return ret;
if (p.connected){
if (!strcmp(trap_CG_Argv(0), "chat") || !strcmp(trap_CG_Argv(0), "tchat") || !strcmp(trap_CG_Argv(0), "enc_chat")){
char s[MAX_SAY_TEXT] = { 0 };
strcpy_s(s, sizeof(s), trap_CG_Argv(1));
for (int i = 0; i < MAX_SAY_TEXT; i++){
if (s[i] == '\n'){
s[i] = '\0';
break;
}
}
//if (!strcmp(trap_CG_Argv(0), "enc_chat")) //damn zero's new cross server chat system!
ReplaceSpecialChar(s);
Network.Send(s, sizeof(s));
}
}
}
//network.cpp
#define PORT 8888
#define DESTIP "127.0.0.1"
pNetwork Network;
void pNetwork::Stop()
{
if (p.connected){
p.connected = false;
shutdown(s, 2);
closesocket(s);
WSACleanup();
}
}
bool pNetwork::Connect()
{
if (p.connected)
this->Stop();
if (!p.wantConnection)
return false;
WSADATA wsadata;
int error = WSAStartup(0x0202, &wsadata);
if (error){
return false;
}
if (wsadata.wVersion != 0x0202)
{
WSACleanup();
return false;
}
s= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET){
WSACleanup();
return false;
}
SOCKADDR_IN target;
target.sin_family = AF_INET;
target.sin_addr.S_un.S_addr = inet_addr(DESTIP);
target.sin_port = htons(PORT);
if (connect(s, (SOCKADDR*)&target, sizeof(target)) == SOCKET_ERROR)
{
WSACleanup();
return false;
}
return true;
}
int pNetwork::Send(char* msg, int len)
{
int ret = send(s, msg, len, 0);
if (ret <= 0){
p.connected = this->Connect();
if (p.connected)
ret = send(s, msg, len, 0);
}
return ret;
}
int pNetwork::Recv(char* msg, int len)
{
int i = shutdown(s, 1);
if (i < 0)
this->Stop();
return recv(s, msg, len, 0);
}
Here is the C# application code.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.Web;
namespace Server_test
{
public partial class Form1 : Form
{
private List PlayerList = new List();
public System.Windows.Forms.TextBox textBox2;
private Server _server;
private bool _colourCodes = false;
private bool _translate = false;
private bool _saveOnExit = false;
public Form1()
{
InitializeComponent();
this.Text = "Logme";
}
private void Form1_Load(object sender, EventArgs e)
{
_server = new Server(this);
GetPlayerData();
}
public void AppendText(String text)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(AppendText), new object[] { text });
return;
}
this.textBox1.AppendText(RemoveColour(text));
this.textBox1.AppendText(Environment.NewLine);
this.textBox4.AppendText(text);
this.textBox4.AppendText(Environment.NewLine);
if (_translate)
{
string trans = (TranslateET(text));
if (trans != null)
{
this.textBox5.AppendText(trans);
this.textBox5.AppendText(Environment.NewLine);
}
}
}
public static string RemoveColour(string s)
{
while (s.Contains("^"))
{
var toRemove = s.Substring(s.IndexOf("^"), 2);
s = s.Replace(toRemove, "");
}
return s;
}
private void GetPlayerData()
{
UdpClient udpClient = new UdpClient();
udpClient.Connect("future.etjump.com", 27960);
Byte[] prefix = { 0xff, 0xff, 0xff, 0xff };
Byte[] command = Encoding.ASCII.GetBytes("getstatus");
Byte[] sendBytes = prefix.Concat(command).ToArray();
udpClient.Send(sendBytes, sendBytes.Length);
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 0);
Byte[] recvBytes = udpClient.Receive(ref ipEndPoint);
udpClient.Close();
PlayerList.Clear();
string[] raw = Encoding.ASCII.GetString(recvBytes).Split('"', '"');
for (int i = 1, j = 0; i < raw.Length; i += 2, j++)
{
var p = raw[i];
//p.name = raw[i];
PlayerList.Add(p);
}
PrintPlayerList();
}
private void PrintPlayerList()
{
textBox3.Clear();
foreach (var p in PlayerList)
{
textBox3.AppendText(RemoveColour(p) + Environment.NewLine);
}
}
private void button1_Click(object sender, EventArgs e)
{
GetPlayerData();
}
private void button2_Click(object sender, EventArgs e)
{
// MessageBox.Show("Sending data");
if (textBox6.Text != null)
{
// MessageBox.Show("Sending: " + textBox6.Text);
_server.HandleSend(textBox6.Text);
textBox6.Clear();
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_server.Shutdown();
if (_saveOnExit)
{
var cwd = Directory.GetCurrentDirectory() + "\\";
int i = 0;
while (File.Exists(cwd + "log" + i + ".log"))
{
i++;
}
File.WriteAllText(cwd+"log"+i+".log", textBox1.Text);
File.WriteAllText(cwd + "log_colour" + i + ".log", textBox4.Text);
}
}
private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
_colourCodes = !_colourCodes;
if (_colourCodes)
{
textBox1.Hide();
textBox4.Show();
}
else
{
textBox1.Show();
textBox4.Hide();
}
}
private void checkBox2_CheckedChanged(object sender, EventArgs e)
{
_translate = !_translate;
}
private void checkBox3_CheckedChanged(object sender, EventArgs e)
{
_saveOnExit = !_saveOnExit;
}
private string TranslateET(string text)
{
string pname;
string toTranslate;
try
{
pname = text.Substring(0, text.IndexOf("^7:"));
}
catch
{
return null;
}
pname = RemoveColour(pname);
toTranslate = text.Substring(text.IndexOf("^7:")+3);
toTranslate = RemoveColour(toTranslate);
var translated = TranslateGoogle(toTranslate);
return pname + ": " + translated;
}
//from http://weblog.west-wind.com/posts/2011/Aug/06/Translating-with-Google-Translate-without-API-and-C-Code
//changed a bit
public string TranslateGoogle(string text)
{
string url = string.Format(@"http://translate.google.com/translate_a/t?client=j&text={0}&hl=en&sl=auto&tl=en",
HttpUtility.UrlEncode(text));
// Retrieve Translation with HTTP GET call
string html = null;
try
{
WebClient web = new WebClient();
// MUST add a known browser user agent or else response encoding doen't return UTF-8 (WTF Google?)
web.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/5.0");
web.Headers.Add(HttpRequestHeader.AcceptCharset, "UTF-8");
// Make sure we have response encoding to UTF-8
web.Encoding = Encoding.UTF8;
html = web.DownloadString(url);
}
catch
{
return null;
}
// Extract out trans":"...[Extracted]...","from the JSON string
string result = Regex.Match(html, "trans\":(\".*?\"),\"", RegexOptions.IgnoreCase).Groups[1].Value;
if (string.IsNullOrEmpty(result))
{
}
// Result is a JavaScript string so we need to deserialize it properly
JavaScriptSerializer ser = new JavaScriptSerializer();
return ser.Deserialize(result, typeof(string)) as string;
}
private void KeyPress(object sender, KeyPressEventArgs e)
{
// if(e.KeyChar == Key.)
}
private void textBox6_Enter(object sender, EventArgs e)
{
ActiveForm.AcceptButton = button2;
}
private void textBox6_Leave(object sender, EventArgs e)
{
ActiveForm.AcceptButton =null;
}
}
}
#pragma once
#define WIN32_LEAN_AND_MEAN
#define VC_EXTRALEAN
#include
#include
#include
#include
#include
//et
#include ETSDK\\src\\cgame\\cg_local.h
#include ETSDK\\src\\ui\\ui_public.h
#include ETSDK\\src\\qcommon\\vm_local.h
//detours
#include Detours\\detours.h
//2.6 offsets
#define OFF_SYSCALL 0x004488A0
#define OFF_GLCONFIG 0x15D0924
#define OFF_FRAMETIME 0x13C08E0
#define OFF_ADDCOMMAND 0x41FD20
#define OFF_VIEWX 0x013EEA8C
#define OFF_VIEWY 0x013EEA88
#define OFF_INBUTTONS 0x835AF8
typedef HINSTANCE(__stdcall *LoadLibraryA_t) (LPCSTR lpLibName);
extern LoadLibraryA_t orig_LoadLibraryA;
HINSTANCE __stdcall p_LoadLibraryA(LPCSTR lpLibName);
typedef int(__cdecl *Syscall_t)(int cmd, ...);
extern Syscall_t orig_syscall;
typedef int(__cdecl *vmMain_t) (int command, ...);
extern vmMain_t orig_vmMain;
int __cdecl vmMain(int command, ...);
typedef void(__cdecl *Cmd_AddCommand_t)(const char *cmd_name, xcommand_t function);
extern Cmd_AddCommand_t orig_Cmd_AddCommand;
typedef void(__cdecl *CG_DrawDisconnect_t)(void);
void __cdecl pCG_DrawDisconnect(void);
extern CG_DrawDisconnect_t orig_CG_DrawDisconnect;
struct kbutton{
int down[2];
unsigned downtime;
unsigned msec;
int active;
int wasPressed;
};
struct game{
glconfig_t *glconfig;
float *vax;
float *vay;
int *com_frametime;
kbutton *jumpbtn;
};
struct cgame{
float ourvax;
float speed;
float optAng;
float velAng;
float accAng;
float optStart;
float angleDiff;
bool touchingGround;
bool dobot;
//downkeys
bool w;
bool s;
bool a;
bool d;
};
struct settings{
bool _bot;
bool _jump;
float _offset = 0;
float _startoffset = 0.3;
int _startspeed = 450;
};
class AutoBot {
public:
void intention(HINSTANCE);
void syshook(bool);
void ethook(bool);
void cghook(bool);
int CG_Shutdown();
//private:
char _path[MAX_PATH + 1];
HMODULE _cghandle;
HMODULE _etprocess;
HMODULE _dllprocess;
game _game;
cgame _cg;
playerState_t *_ps;
settings _settings;
bool _ethooked;
bool _syshooked;
bool _cghooked;
bool _youtube;
char _currentURL[512];
};
extern AutoBot bot;
Sadly even with all the formulae on calculating the perfect angles, I never quite got there myself. And so the actual angle calculation comes from Setups trickjump bot. Even now I don't understand the magic numbers that come from ET SDK, but it works.
#include Autobot.h
#include commands.h
AutoBot bot;
LoadLibraryA_t orig_LoadLibraryA = NULL;
Syscall_t orig_syscall = NULL;
vmMain_t orig_vmMain = NULL;
Cmd_AddCommand_t orig_Cmd_AddCommand = NULL;
CG_DrawDisconnect_t orig_CG_DrawDisconnect = NULL;
//forward decs
int __cdecl vmMain(int command, ...);
bool bDataCompare(const BYTE *pData, const BYTE *bMask, const char *szMask);
DWORD dwFindPattern(DWORD dwAddress, DWORD dwLen, BYTE *bMask, char *szMask);
void startbot();
void autojump();
void anglebot();
//etsdk
float AngleNormalize180(float angle);
float AngleNormalize360(float angle);
float AngleDelta(float angle1, float angle2);
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
{
bot.intention(hinstDLL);
}
}
return true;
}
HINSTANCE __stdcall p_LoadLibraryA(LPCSTR lpLibName)
{
HMODULE hm = orig_LoadLibraryA(lpLibName);
if (strstr(lpLibName, "cgame_mp_x86.dll")){
bot._cghandle = hm;
bot.cghook(true);
bot._dllprocess = GetModuleHandleA(lpLibName);
}
return hm;
}
void AutoBot::intention(HINSTANCE hinst)
{
int len = GetModuleFileNameA(hinst, _path, sizeof(_path));
bool ret = len;
while (len && _path[len] != '\\') --len;
_path[len + 1] = '\0';
syshook(ret);
ethook(ret);
}
void AutoBot::syshook(bool state)
{
static bool hooked;
if (state && !hooked) {
orig_LoadLibraryA = (LoadLibraryA_t)DetourFunction((BYTE*)LoadLibraryA, (BYTE*)p_LoadLibraryA);
_game.glconfig = (glconfig_t*)OFF_GLCONFIG;
_game.com_frametime = (int*)OFF_FRAMETIME;
_game.vax = (float*)OFF_VIEWX;
_game.vay = (float*)OFF_VIEWY;
_game.jumpbtn = (kbutton*)((DWORD)OFF_INBUTTONS - 2 * sizeof(kbutton));
}
else if (hooked)
DetourRemove((BYTE*)orig_LoadLibraryA, (BYTE*)p_LoadLibraryA);
hooked = state;
}
void AutoBot::ethook(bool state)
{
static bool hooked = false;
if (state && !hooked){
orig_syscall = (Syscall_t)OFF_SYSCALL;
orig_Cmd_AddCommand = (Cmd_AddCommand_t)OFF_ADDCOMMAND;
Commands::RegisterCommands();
hooked = true;
}
}
void AutoBot::cghook(bool state)
{
if (state && !_cghooked){
orig_vmMain = (vmMain_t)DetourFunction((BYTE*)GetProcAddress(_cghandle, "vmMain"), (BYTE*)vmMain);
//2.0.5RC1
_ps = (playerState_t*)((*(DWORD*)(dwFindPattern((DWORD)_cghandle, (DWORD)(_cghandle + 0x02000000),
(BYTE*)"\x68\x00\x00\x00\x00\x0F\x44\xD9", "x????xxx") + 1)));
//orig_CG_DrawDisconnect = (CG_DrawDisconnect_t)DetourFunction((BYTE*)(dwFindPattern((DWORD)_cghandle, (DWORD)(_cghandle + 0x02000000),
// (BYTE*)"\x55\x8B\xEC\x83\xEC\x1C\x83\x3D\x00\x00\x00\x00\x00\x74\x19", "xxxxxxxx?????xx")), (BYTE*)pCG_DrawDisconnect);
}
else if (_cghooked) {
DetourRemove((BYTE*)orig_vmMain, (BYTE*)vmMain);
//DetourRemove((BYTE*)orig_CG_DrawDisconnect, (BYTE*)pCG_DrawDisconnect);
}
_cghooked = true;
}
int AutoBot::CG_Shutdown(){
cghook(false);
_cghooked = false;
int r = orig_vmMain(CG_SHUTDOWN);
ethook(true);
return r;
}
void __cdecl pCG_DrawDisconnect(void)
{
return;
}
int __cdecl vmMain(int command, ...)
{
int arg[12];
va_list arglist;
va_start(arglist, command);
int count = 0;
for (; count < 12; count++)
arg[count] = va_arg(arglist, int);
va_end(arglist);
switch (command)
{
case CG_INIT: {
orig_vmMain(command, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7], arg[8], arg[9], arg[10], arg[11]);
bot._game.glconfig->vidWidth / 640.f;
bot._game.glconfig->vidHeight / 480.0f;
return 0;
}
case CG_SHUTDOWN:{
return bot.CG_Shutdown();
break;
}
case CG_DRAW_ACTIVE_FRAME:{
usercmd_t cmd;
int cmdnum = 0;
cmdnum = orig_syscall(CG_GETCURRENTCMDNUMBER);
orig_syscall(CG_GETUSERCMD, cmdnum, &cmd);
bot._cg.ourvax = *bot._game.vax + SHORT2ANGLE(bot._ps->delta_angles[1]);
if (bot._cg.ourvax > 180.f) bot._cg.ourvax -= 360.f;
if (bot._cg.ourvax < -180.f) bot._cg.ourvax += 360.f;
float velx = bot._ps->velocity[0];
float vely = bot._ps->velocity[1];
bot._cg.speed = sqrt(velx*velx + vely*vely);
bot._cg.optAng = bot._cg.optStart = AngleNormalize180(RAD2DEG(acos(317.44f / bot._cg.speed *1.1)));
bot._cg.velAng = RAD2DEG(atan2(vely, velx));
bot._cg.accAng = RAD2DEG(atan2((float)-cmd.rightmove, (float)cmd.forwardmove));
bot._cg.d = bot._cg.a = bot._cg.s = bot._cg.w = false;
if (cmd.rightmove > 0) bot._cg.d = true;
if (cmd.forwardmove > 0) bot._cg.w = true;
if (cmd.rightmove < 0) bot._cg.a = true;
if (cmd.forwardmove < 0) bot._cg.s = true;
if (bot._cg.a && !bot._cg.d){
bot._cg.angleDiff = (AngleDelta(bot._cg.ourvax + bot._cg.accAng, bot._cg.velAng + bot._cg.optAng)) - 0.3;
bot._cg.dobot = true;
}
else if
(bot._cg.d && !bot._cg.a){
bot._cg.angleDiff = (AngleDelta(bot._cg.ourvax + bot._cg.accAng, bot._cg.velAng - bot._cg.optAng)) + 0.3;
bot._cg.dobot = true;
}
else
bot._cg.dobot = false;
break;
}
}
int ret = orig_vmMain(command, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7], arg[8], arg[9], arg[10], arg[11]);
switch (command)
{
case CG_DRAW_ACTIVE_FRAME:{
if (bot._settings._jump) autojump();
if (bot._settings._bot) anglebot();
break;
}
}
return ret;
}
//botstuff
void autojump()
{
if (bot._ps->groundEntityNum != ENTITYNUM_NONE){
bot._game.jumpbtn->active = bot._game.jumpbtn->wasPressed = 1;
bot._game.jumpbtn->down[0] = -1;
bot._game.jumpbtn->downtime = *bot._game.com_frametime;
}
else
bot._game.jumpbtn->active = bot._game.jumpbtn->down[0] = bot._game.jumpbtn->down[1] = 0;
}
void startbot()
{
bool justWS = ((bot._cg.w || bot._cg.s) && (!bot._cg.a && !bot._cg.d));
if (bot._cg.speed < 321 || justWS) return;
if (bot._cg.speed < bot._settings._startspeed){
if (bot._cg.a)
*(float *)bot._game.vax -= bot._cg.angleDiff + bot._settings._startoffset;
else
*(float *)bot._game.vax -= bot._cg.angleDiff - bot._settings._startoffset;
}
}
void anglebot()
{
//if (bot._cg.speed > bot._settings._startspeed && bot._cg.dobot)
// *(float*)bot._game.vax -= bot._cg.angleDiff;
float offset = bot._settings._offset;
if (bot._cg.speed > bot._settings._startspeed){
if ((bot._cg.a && !bot._cg.d) || (bot._cg.w && bot._cg.a && bot._cg.d) || (bot._cg.w && !bot._cg.a && !bot._cg.d))
*(float *)bot._game.vax -= AngleDelta(bot._cg.ourvax + bot._cg.accAng, bot._cg.velAng + bot._cg.optAng) - 0.3 - offset;
else if ((bot._cg.d && !bot._cg.a) || (bot._cg.s && bot._cg.a && bot._cg.d) || (bot._cg.s && !bot._cg.a && !bot._cg.d))
*(float *)bot._game.vax -= AngleDelta(bot._cg.ourvax + bot._cg.accAng, bot._cg.velAng - bot._cg.optAng) + 0.3 + offset;
}
}
//etsdk
float AngleNormalize180(float angle) {
angle = AngleNormalize360(angle);
if (angle > 180.0)
angle -= 360.0;
return angle;
}
float AngleNormalize360(float angle) {
return (360.0 / 65536) * ((int)(angle * (65536 / 360.0)) & 65535);
}
float AngleDelta(float angle1, float angle2) {
return AngleNormalize180(angle1 - angle2);
}
//fromweb -- worst findpattern there is :D
bool bDataCompare(const BYTE *pData, const BYTE *bMask, const char *szMask)
{
for (; *szMask; ++szMask, ++pData, ++bMask)
if (*szMask == 'x' && *pData != *bMask)
return false;
return (*szMask) == NULL;
}
DWORD dwFindPattern(DWORD dwAddress, DWORD dwLen, BYTE *bMask, char *szMask)
{
for (DWORD i = 0; i < dwLen; i++)
if (bDataCompare((BYTE*)(dwAddress + i), bMask, szMask))
return (DWORD)(dwAddress + i);
return 0;
}
This is just a little spammer function that worked for either youtube in browser or winamp, had to let the server know what I was listening to....
BOOL CALLBACK callback_createspam(HWND hwnd, LPARAM lparam)
{
bot._youtube = false;
char buf[512];
GetWindowTextA(hwnd, buf, sizeof(buf));
char* strend = strstr(buf, " &foobar");
if (strend){
*strend = '\0';
sprintf_s(wintitle, sizeof(wintitle), "say \"^7NP: ^o%s\"\n", buf);
return false;
}
strend = strstr(buf, " - Winamp");
if (strend){
*strend = '\0';
int i = 0, j = -1;
for (; buf[i] != ' '; ++i);
while (buf[i] != '\0') buf[++j] = buf[++i]; buf[j] = '\0';
sprintf_s(wintitle, sizeof(wintitle), "say \"^7NP: ^o%s\"\n", buf);
return false;
}
strend = strstr(buf, " - YouTube");
if (strend){
bot._youtube = true;
*strend = '\0';
sprintf_s(wintitle, sizeof(wintitle), "say \"^7NP: ^o%s\"\n", buf);
return false;
}
return true;
}