1424 lines
31 KiB
C
1424 lines
31 KiB
C
/*****************************************************************
|
|
|
|
Eraser Bot source code - by Ryan Feltrin, Added to by Acrid-
|
|
|
|
..............................................................
|
|
|
|
This file is Copyright(c) 1998, Ryan Feltrin, All Rights Reserved.
|
|
|
|
..............................................................
|
|
|
|
All other files are Copyright(c) Id Software, Inc.
|
|
|
|
Please see liscense.txt in the source directory for the copyright
|
|
information regarding those files belonging to Id Software, Inc.
|
|
|
|
..............................................................
|
|
|
|
Should you decide to release a modified version of Eraser, you MUST
|
|
include the following text (minus the BEGIN and END lines) in the
|
|
documentation for your modification.
|
|
|
|
--- BEGIN ---
|
|
|
|
The Eraser Bot is a product of Ryan Feltrin, and is available from
|
|
the Eraser Bot homepage, at http://impact.frag.com.
|
|
|
|
This program is a modification of the Eraser Bot, and is therefore
|
|
in NO WAY supported by Ryan Feltrin.
|
|
|
|
This program MUST NOT be sold in ANY form. If you have paid for
|
|
this product, you should contact Ryan Feltrin immediately, via
|
|
the Eraser Bot homepage.
|
|
|
|
--- END ---
|
|
|
|
..............................................................
|
|
|
|
You will find p_trail.c has not been included with the Eraser
|
|
source code release. This is NOT an error. I am unable to
|
|
distribute this file because it contains code that is bound by
|
|
legal documents, and signed by myself, never to be released
|
|
to the public. Sorry guys, but law is law.
|
|
|
|
I have therefore include the compiled version of these files
|
|
in .obj form in the src\Release and src\Debug directories.
|
|
So while you cannot edit and debug code within these files,
|
|
you can still compile this source as-is. Although these will only
|
|
work in MSVC v5.0, linux versions can be made available upon
|
|
request.
|
|
|
|
NOTE: When compiling this source, you will get a warning
|
|
message from the compiler, regarding the missing p_trail.c
|
|
file. Just ignore it, it will still compile fine.
|
|
|
|
..............................................................
|
|
|
|
I, Ryan Feltrin/Acrid-, hold no responsibility for any harm caused by the
|
|
use of this source code. I also am NOT willing to provide any form
|
|
of help or support for this source code. It is provided as-is,
|
|
as a service by me, with no documentation, other then the comments
|
|
contained within the code. If you have any queries, I suggest you
|
|
visit the "official" Eraser source web-board, at
|
|
http://www.telefragged.com/epidemic/. I will stop by there from
|
|
time to time, to answer questions and help with any problems that
|
|
may arise.
|
|
|
|
Otherwise, have fun, and I look forward to seeing what can be done
|
|
with this.
|
|
|
|
-Ryan Feltrin
|
|
-Acrid-
|
|
|
|
*****************************************************************/
|
|
#include "g_local.h"
|
|
#include "bot_procs.h"
|
|
#include "m_player.h"
|
|
|
|
void CopyToBodyQue (edict_t *ent);
|
|
|
|
void bot_AnimateFrames(edict_t *self)
|
|
{
|
|
int minframe, maxframe;
|
|
vec3_t themove;
|
|
|
|
VectorSubtract(self->s.origin, self->animate_org, themove);
|
|
VectorCopy(self->s.origin, self->animate_org);
|
|
|
|
themove[2] = 0;
|
|
|
|
if (self->s.modelindex != 255)
|
|
{ // gibbed, ready to spawn
|
|
self->s.frame = 0;
|
|
|
|
if (self->timestamp < (level.time - 2)) // wait a bit
|
|
respawn_bot(self);
|
|
|
|
return;
|
|
}
|
|
//NEW TESTING botfreeze note needed more than 1 frame to body didnt disappear..
|
|
if (self->s.frame >= FRAME_stand01 && self->frozen &&
|
|
self->health <=0)
|
|
{
|
|
self->client->anim_priority = ANIM_DEATH;
|
|
|
|
if ((self->s.frame >= FRAME_stand01) && (self->s.frame < FRAME_stand03))
|
|
{
|
|
self->s.frame++;
|
|
}
|
|
else // must be ready to spawn
|
|
{
|
|
CopyToBodyQue(self);
|
|
respawn_bot(self);
|
|
}
|
|
|
|
return;
|
|
}
|
|
//NEW TESTING
|
|
if (self->s.frame >= FRAME_crdeath1)
|
|
{
|
|
self->client->anim_priority = ANIM_DEATH;
|
|
|
|
if (((self->s.frame >= FRAME_crdeath1) && (self->s.frame < FRAME_crdeath5)) ||
|
|
((self->s.frame >= FRAME_death101) && (self->s.frame < FRAME_death106)) ||
|
|
((self->s.frame >= FRAME_death201) && (self->s.frame < FRAME_death206)) ||
|
|
((self->s.frame >= FRAME_death301) && (self->s.frame < FRAME_death308)))
|
|
{
|
|
self->s.frame++;
|
|
}
|
|
else // must be ready to spawn
|
|
{
|
|
CopyToBodyQue(self);
|
|
respawn_bot(self);
|
|
}
|
|
|
|
return;
|
|
}
|
|
//NEW TESTING
|
|
else if (self->frozen && self->health <= 0)//acrid 3/99 botfrozen death animation
|
|
{
|
|
self->s.frame = FRAME_stand01;
|
|
}
|
|
//NEW TESTING
|
|
else if (self->health <= 0 && !self->frozen) // FIXME: this shouldn't happen, but it does
|
|
{ // pick a random death frame, and go with it
|
|
float rnd;
|
|
|
|
rnd = random() * 3;
|
|
|
|
if (self->viewheight < 0)
|
|
self->s.frame = FRAME_crdeath1;
|
|
else if (rnd <= 1)
|
|
self->s.frame = FRAME_death101;
|
|
else if (rnd <= 2)
|
|
self->s.frame = FRAME_death201;
|
|
else
|
|
self->s.frame = FRAME_death301;
|
|
}
|
|
else if (self->client->anim_priority == ANIM_PAIN)
|
|
{
|
|
self->s.frame++;
|
|
|
|
if (self->s.frame > self->client->anim_end)
|
|
{
|
|
self->s.frame = FRAME_stand01;
|
|
self->client->anim_priority = ANIM_BASIC;
|
|
}
|
|
|
|
}
|
|
else if (!self->groundentity && (self->waterlevel || (self->last_inair > (level.time - 0.3))))
|
|
{ // jumping
|
|
self->client->anim_priority = ANIM_JUMP;
|
|
|
|
minframe = FRAME_jump1;
|
|
maxframe = FRAME_jump3;
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame < minframe)
|
|
self->s.frame = minframe;
|
|
else if (self->s.frame > maxframe)
|
|
self->s.frame = maxframe;
|
|
}
|
|
else if ((self->s.frame >= FRAME_jump3) &&
|
|
(self->s.frame < FRAME_jump6)) // jump landing
|
|
{
|
|
self->client->anim_priority = ANIM_JUMP;
|
|
|
|
maxframe = FRAME_jump3;
|
|
|
|
self->s.frame++;
|
|
}
|
|
else if ((self->s.frame <= FRAME_point12) && (self->s.frame >= FRAME_salute01))
|
|
{
|
|
self->client->anim_priority = ANIM_WAVE;
|
|
|
|
self->s.frame++;
|
|
|
|
if (self->enemy || (self->s.frame > self->radius_dmg)) // abort if enemy found
|
|
self->s.frame = FRAME_stand01;
|
|
}
|
|
else if ((self->s.frame >= FRAME_attack1) && (self->s.frame <= FRAME_attack3))
|
|
{
|
|
self->client->anim_priority = ANIM_ATTACK;
|
|
|
|
maxframe = FRAME_attack3;
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame > maxframe)
|
|
self->s.frame = FRAME_run1; // finshed firing, loop is created by each fire, in bot_Attack()
|
|
}
|
|
else if ((self->s.frame >= FRAME_crattak1) && (self->s.frame < FRAME_crattak3))
|
|
{
|
|
self->client->anim_priority = ANIM_ATTACK;
|
|
|
|
maxframe = FRAME_crattak3;
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame > maxframe)
|
|
self->s.frame = FRAME_run1; // finshed firing, loop is created by each fire, in bot_Attack()
|
|
}
|
|
else if (VectorLength(themove) > 4) // running
|
|
{
|
|
self->client->anim_priority = ANIM_BASIC;
|
|
|
|
if (self->maxs[2] == 4)
|
|
{
|
|
minframe = FRAME_crwalk1;
|
|
maxframe = FRAME_crwalk6;
|
|
}
|
|
else
|
|
{
|
|
minframe = FRAME_run1;
|
|
maxframe = FRAME_run6;
|
|
|
|
if (!self->waterlevel && self->client->pickup_msg_time < (level.time - 0.3))
|
|
{
|
|
self->s.event = EV_FOOTSTEP;
|
|
self->client->pickup_msg_time = level.time;
|
|
}
|
|
}
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame < minframe)
|
|
self->s.frame = minframe;
|
|
else if (self->s.frame > maxframe)
|
|
self->s.frame = minframe;
|
|
|
|
}
|
|
else // standing
|
|
{
|
|
self->client->anim_priority = ANIM_BASIC;
|
|
|
|
if (self->maxs[2] == 4)
|
|
{
|
|
minframe = FRAME_crstnd01;
|
|
maxframe = FRAME_crstnd19;
|
|
}
|
|
else
|
|
{
|
|
minframe = FRAME_stand01;
|
|
maxframe = FRAME_stand40;
|
|
}
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame < minframe)
|
|
self->s.frame = minframe;
|
|
else if (self->s.frame > maxframe)
|
|
self->s.frame = minframe;
|
|
}
|
|
|
|
}
|
|
|
|
void botDebugPrint(char *msg, ...)
|
|
{
|
|
if (!bot_debug->value)
|
|
return;
|
|
else
|
|
{
|
|
char bigbuffer[0x10000];
|
|
int len;
|
|
va_list argptr;
|
|
|
|
va_start (argptr,msg);
|
|
len = vsprintf (bigbuffer,msg,argptr);
|
|
va_end (argptr);
|
|
gi.dprintf(bigbuffer);
|
|
}
|
|
}
|
|
|
|
void ReadBotChat(void)
|
|
{
|
|
FILE *f;
|
|
int section_index, line_count, i;
|
|
char filename[256];
|
|
char buffer;
|
|
cvar_t *game_dir;
|
|
|
|
game_dir = gi.cvar ("game", "", 0);
|
|
|
|
#ifdef _WIN32
|
|
i = sprintf(filename, ".\\");
|
|
i += sprintf(filename + i, game_dir->string);
|
|
i += sprintf(filename + i, "\\chat.txt");
|
|
#else
|
|
strcpy(filename, "./");
|
|
strcat(filename, game_dir->string);
|
|
strcat(filename, "/chat.txt");
|
|
#endif
|
|
|
|
f = fopen (filename, "r");
|
|
if (!f)
|
|
{
|
|
gi.error("\nUnable to read chat.txt\nChat functions not available.\n\n");
|
|
return;
|
|
}
|
|
|
|
memset(bot_chat_text, 0, sizeof(bot_chat_text));
|
|
|
|
section_index = -1;
|
|
|
|
while (!feof(f))
|
|
{
|
|
fscanf(f, "%c", &buffer);
|
|
|
|
if (buffer == '#')
|
|
{ // read to the end of the line
|
|
while (!feof(f) && (buffer != '\n'))
|
|
fscanf(f, "%c", &buffer);
|
|
}
|
|
else if (buffer == '-')
|
|
{
|
|
// increment section
|
|
section_index++;
|
|
line_count = -1;
|
|
|
|
while (!feof(f) && (buffer != '\n'))
|
|
fscanf(f, "%c", &buffer);
|
|
}
|
|
else if (((buffer >= 'a') && (buffer <= 'z')) ||
|
|
((buffer >= 'A') && (buffer <= 'Z')) ||
|
|
(buffer == '%'))
|
|
{ // read this entire line
|
|
i = 0;
|
|
line_count++;
|
|
|
|
// allocate memory for new string
|
|
bot_chat_text[section_index][line_count] = gi.TagMalloc(256, TAG_GAME);
|
|
memset(bot_chat_text[section_index][line_count], 0, 256);
|
|
|
|
while (!feof(f) && (buffer != '\n'))
|
|
{
|
|
bot_chat_text[section_index][line_count][i++] = buffer;
|
|
|
|
fscanf(f, "%c", &buffer);
|
|
}
|
|
|
|
if (i > 0)
|
|
{
|
|
bot_chat_text[section_index][line_count][i] = '\0';
|
|
}
|
|
|
|
// update the count now
|
|
bot_chat_count[section_index] = line_count;
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
//FIXME ACRID GOES WITH BOTS.CFG
|
|
gitem_t *GetWeaponForNumber(int i)
|
|
{
|
|
switch (i)
|
|
{
|
|
case 2 :
|
|
return item_shotgun;
|
|
case 3 :
|
|
return item_supershotgun;
|
|
case 4 :
|
|
return item_machinegun;
|
|
case 5 :
|
|
return item_chaingun;
|
|
case 6 :
|
|
return item_grenadelauncher;
|
|
case 7 :
|
|
return item_rocketlauncher;
|
|
case 8 :
|
|
return item_railgun;
|
|
case 9 :
|
|
return item_hyperblaster;
|
|
case 0 :
|
|
return item_bfg10k;
|
|
case 10 :
|
|
return item_sniperrifle;//Acrid test
|
|
case 11 :
|
|
return item_lightninggun;
|
|
case 12 :
|
|
return item_infecteddart;
|
|
case 13 :
|
|
return item_pulsecannon;
|
|
case 14 :
|
|
return item_telsacoil;
|
|
case 15 :
|
|
return item_flamethrower;
|
|
case 16 :
|
|
return item_pelletrocketlauncher;
|
|
case 17 :
|
|
return item_rocketnapalmlauncher;
|
|
case 18 :
|
|
return item_rocketclusterlauncher;
|
|
case 19 :
|
|
return item_needler;
|
|
case 20 :
|
|
return item_shc;
|
|
case 21 :
|
|
return item_handgrenades;
|
|
case 22 :
|
|
return item_poisondart;//armordart
|
|
case 23 :
|
|
return item_ak47;
|
|
case 24 :
|
|
return item_pistol;
|
|
case 25 :
|
|
return item_stingerrocketlauncher;
|
|
case 26 :
|
|
return item_knife;
|
|
default:
|
|
gi.dprintf("GetWeaponForNumber: Unknown item number: %d\n",i);
|
|
return item_shotgun;
|
|
}
|
|
}
|
|
//ACRID I THINK THIS IS READING NON TEAM LISTING FOR TEAMS DATA,SKINS, SCORE
|
|
bot_info_t *GenerateBotData(bot_team_t *bot_team, char *botname)
|
|
{
|
|
int i, gen;
|
|
bot_info_t *botdata;
|
|
|
|
botdata = gi.TagMalloc (sizeof(bot_info_t), TAG_GAME);
|
|
|
|
botdata->ingame_count = 0;
|
|
|
|
// name
|
|
botdata->name = gi.TagMalloc (128, TAG_GAME);
|
|
strcpy(botdata->name, botname);
|
|
|
|
// skin
|
|
botdata->skin = gi.TagMalloc (128, TAG_GAME);
|
|
strcpy(botdata->skin, bot_team->default_skin);
|
|
|
|
i = gen = 0;
|
|
while (botname[i])
|
|
{
|
|
gen += botname[i] + i;
|
|
i++;
|
|
}
|
|
|
|
// stats
|
|
botdata->bot_stats.accuracy = (float) (1 + ((int)(gen*3.234) % 5));
|
|
botdata->bot_stats.aggr = (float) (1 + ((int)(gen*5.132) % 5));
|
|
botdata->bot_stats.combat = (float) (1 + ((int)(gen*4.476) % 5));
|
|
|
|
botdata->bot_stats.fav_weapon = GetWeaponForNumber(3 + ((int)(gen*2.356) % 7));
|
|
|
|
botdata->bot_stats.quad_freak = (((int)(gen*1.453) % 3) < 2);
|
|
botdata->bot_stats.camper = (((int)(gen*2.376) % 3) == 2);
|
|
botdata->bot_stats.avg_ping = ((int)(gen*0.678) % 4) * 100;
|
|
|
|
return botdata;
|
|
}
|
|
|
|
// Advances the pointer to the FILE to the next non-space, non-tab character
|
|
char next_nonspace(FILE **f)
|
|
{
|
|
char ch=9;
|
|
|
|
while (!feof(*f) && ((ch==9) || (ch==' ')))
|
|
{
|
|
fscanf(*f, "%c", &ch); // fscanf will skip spaces, but not TABS
|
|
}
|
|
|
|
return ch;
|
|
};
|
|
|
|
bot_team_t *ReadTeamData(FILE **f)
|
|
{
|
|
char strbuf[256], ch;
|
|
int i, numbots;
|
|
bot_team_t *bot_team;
|
|
|
|
bot_team = gi.TagMalloc (sizeof(bot_team_t), TAG_GAME);
|
|
|
|
// name
|
|
i=0;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
bot_team->teamname = gi.TagMalloc (strlen(strbuf)+1, TAG_GAME);
|
|
strcpy(bot_team->teamname, strbuf);
|
|
|
|
// abbrev
|
|
i=0;
|
|
next_nonspace(f);
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
bot_team->abbrev = gi.TagMalloc (strlen(strbuf)+1, TAG_GAME);
|
|
strcpy(bot_team->abbrev, strbuf);
|
|
|
|
// default model/skin
|
|
i=0;
|
|
next_nonspace(f);
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
bot_team->default_skin = G_CopyString(strbuf);
|
|
bot_team->default_skin = gi.TagMalloc (strlen(strbuf)+1, TAG_GAME);
|
|
strcpy(bot_team->default_skin, strbuf);
|
|
|
|
// start of the bots
|
|
ch = 0;
|
|
while ((ch != '[') && !feof(*f))
|
|
fscanf(*f, "%c", &ch);
|
|
|
|
if (feof(*f))
|
|
{
|
|
gi.error("Bot team \"%s\"doesn't have bot list\n", bot_team->teamname);
|
|
return NULL;
|
|
}
|
|
|
|
fscanf(*f, "%c", &ch); // skip the opening "
|
|
|
|
numbots = 0;
|
|
while ((numbots < MAX_PLAYERS_PER_TEAM) && (ch != ']'))
|
|
{
|
|
// read the name
|
|
|
|
i=0;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
if (!(bot_team->bots[numbots] = GetBotData(strbuf)))
|
|
{
|
|
bot_team->bots[numbots] = GenerateBotData(bot_team, strbuf);
|
|
|
|
// add the bot to the teambot_list
|
|
if (!teambot_list)
|
|
{
|
|
teambot_list = bot_team->bots[numbots];
|
|
teambot_list->next = NULL;
|
|
}
|
|
else // add to the start of the list
|
|
{
|
|
bot_team->bots[numbots]->next = teambot_list;
|
|
teambot_list = bot_team->bots[numbots];
|
|
}
|
|
|
|
}
|
|
|
|
numbots++;
|
|
|
|
if (next_nonspace(f) == ']')
|
|
break; // end of group
|
|
}
|
|
|
|
// clear any remaing bot slots
|
|
while (numbots < MAX_PLAYERS_PER_TEAM)
|
|
bot_team->bots[numbots++] = NULL;
|
|
|
|
bot_team->score = 0;
|
|
bot_team->ingame = 0;
|
|
bot_team->num_players = 0;
|
|
|
|
return bot_team;
|
|
};
|
|
//FIXME ACRID
|
|
// reads a line of bot data from bots.cfg
|
|
bot_info_t *ReadBotData(FILE **f)
|
|
{
|
|
char strbuf[256];
|
|
char buffer;
|
|
int i;
|
|
bot_info_t *botdata;
|
|
|
|
botdata = gi.TagMalloc (sizeof(bot_info_t), TAG_GAME);
|
|
|
|
botdata->ingame_count = 0;
|
|
|
|
// name
|
|
i=0;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
|
|
if (strbuf[i] == '\n')
|
|
return NULL;
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
botdata->name = gi.TagMalloc (128, TAG_GAME);
|
|
strcpy(botdata->name, strbuf);
|
|
|
|
while (!feof(*f) && (buffer != '"'))
|
|
{
|
|
fscanf(*f, "%c", &buffer);
|
|
if (buffer == '\n')
|
|
return NULL;
|
|
}
|
|
|
|
// skin
|
|
i=0;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
if (strbuf[i] == '\n')
|
|
return NULL;
|
|
|
|
while ((strbuf[i] != '\"') && (i < 255))
|
|
{
|
|
i++;
|
|
fscanf(*f, "%c", &strbuf[i]);
|
|
|
|
if (strbuf[i] == '\n')
|
|
return NULL;
|
|
}
|
|
strbuf[i] = '\0'; // strip the trailing "
|
|
|
|
botdata->wfclass = gi.TagMalloc (128, TAG_GAME);//Acrid
|
|
strcpy(botdata->wfclass, strbuf);//Acrid
|
|
|
|
fscanf(*f, "%c", &buffer);
|
|
|
|
if (buffer == '\n')
|
|
return NULL;
|
|
|
|
// stats
|
|
fscanf(*f, "%i", &i);
|
|
botdata->bot_stats.accuracy = (float) i;
|
|
fscanf(*f, "%i", &i);
|
|
botdata->bot_stats.aggr = (float) i;
|
|
fscanf(*f, "%i", &i);
|
|
botdata->bot_stats.combat = (float) i;
|
|
fscanf(*f, "%i", &i);
|
|
|
|
if (i == 1) // overwrite blaster with RL
|
|
i = 7;
|
|
|
|
botdata->bot_stats.fav_weapon = GetWeaponForNumber(i);
|
|
|
|
fscanf(*f, "%i", &(botdata->bot_stats.quad_freak));
|
|
fscanf(*f, "%i", &(botdata->bot_stats.camper));
|
|
fscanf(*f, "%i", &(botdata->bot_stats.avg_ping));
|
|
|
|
return botdata;
|
|
}
|
|
|
|
void ReadViewWeaponModel(FILE **f)
|
|
{
|
|
char buffer;
|
|
int i=0;
|
|
|
|
while (!feof(*f) && (buffer != '\n'))
|
|
{
|
|
fscanf(*f, "%c", &buffer);
|
|
|
|
if ((buffer != '"') && (buffer != '\n') && (buffer != '\r'))
|
|
view_weapon_models[num_view_weapons][i++] = buffer;
|
|
}
|
|
|
|
if (i>0)
|
|
{
|
|
view_weapon_models[num_view_weapons][i] = 0;
|
|
num_view_weapons++;
|
|
}
|
|
}
|
|
|
|
qboolean read_bot_cfg = false;
|
|
|
|
// Reads data from bots.cfg (called from worldspawn)
|
|
void ReadBotConfig()
|
|
{
|
|
FILE *f;
|
|
int i, mode=0; // mode 0 for reading bots, 1 for teams
|
|
char filename[256];
|
|
char buffer;
|
|
bot_info_t *botdata=NULL, *last_botdata=NULL;
|
|
cvar_t *game_dir;
|
|
|
|
if (read_bot_cfg) // must have already read in the config
|
|
return;
|
|
|
|
game_dir = gi.cvar ("game", "", 0);
|
|
|
|
#ifdef _WIN32
|
|
i = sprintf(filename, ".\\");
|
|
i += sprintf(filename + i, game_dir->string);
|
|
i += sprintf(filename + i, "\\bots.cfg");
|
|
#else
|
|
strcpy(filename, "./");
|
|
strcat(filename, game_dir->string);
|
|
strcat(filename, "/bots.cfg");
|
|
#endif
|
|
|
|
f = fopen (filename, "r");
|
|
if (!f)
|
|
{
|
|
gi.error("Unable to read bots.cfg. Cannot continue.\n");
|
|
return;
|
|
}
|
|
|
|
// initialise the teams
|
|
for (i=0; i<MAX_TEAMS; i++)
|
|
bot_teams[i] = NULL;
|
|
|
|
gi.dprintf("\nReading bots.cfg..\n");
|
|
|
|
// Add Eraser, the hard-coded bot
|
|
botinfo_list = gi.TagMalloc (sizeof(bot_info_t), TAG_GAME);
|
|
botinfo_list->ingame_count = 0;
|
|
botinfo_list->name = "Eraser";
|
|
botinfo_list->wfclass = "1";//was skin
|
|
|
|
botinfo_list->bot_stats.accuracy = 5;
|
|
botinfo_list->bot_stats.aggr = 0;
|
|
botinfo_list->bot_stats.combat = 5;
|
|
botinfo_list->bot_stats.fav_weapon = GetWeaponForNumber(2);
|
|
botinfo_list->bot_stats.quad_freak = 0;
|
|
botinfo_list->bot_stats.camper = 0;
|
|
botinfo_list->bot_stats.avg_ping = 50;
|
|
// done.
|
|
|
|
botdata = botinfo_list;
|
|
|
|
total_bots = 1;
|
|
total_teams = 0;
|
|
teambot_list = NULL;
|
|
num_view_weapons = 0;
|
|
memset(view_weapon_models, 0, sizeof(view_weapon_models));
|
|
|
|
while (!feof(f))
|
|
{
|
|
fscanf(f, "%c", &buffer);
|
|
|
|
if (feof(f))
|
|
break;
|
|
|
|
if (buffer == '#') // commented line
|
|
{
|
|
while (!feof(f) && (buffer != '\n'))
|
|
fscanf(f, "%c", &buffer);
|
|
}
|
|
else if (buffer == '[') // mode specifier (bots/teams)
|
|
{
|
|
fscanf(f, "%c", &buffer);
|
|
|
|
if (buffer == 'b')
|
|
mode = 0;
|
|
else if (buffer == 't')
|
|
{
|
|
// if (!teamplay->value /*|| ctf->value*/)
|
|
// break;
|
|
mode = 1;
|
|
}
|
|
else if (buffer == 'v')
|
|
{
|
|
// if (!view_weapons->value)
|
|
break;
|
|
mode = 2;
|
|
}
|
|
|
|
fscanf(f, "\n");
|
|
}
|
|
else if (buffer == '"') // start of some data
|
|
{
|
|
if (mode == 0)
|
|
{
|
|
last_botdata = botdata;
|
|
if (!(botdata = ReadBotData(&f)))
|
|
{
|
|
gi.error("\nError in BOTS.CFG: Invalid BOT (#%i)\nEither re-install Eraser, or check your bots.cfg file for errors\n\n", total_bots);
|
|
break;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
if (!_stricmp(botdata->name, "Eraser"))
|
|
#else
|
|
if (!strcasecmp(botdata->name, "Eraser"))
|
|
#endif
|
|
{ // ignore this bot
|
|
gi.TagFree(botdata);
|
|
botdata = last_botdata;
|
|
}
|
|
else
|
|
{
|
|
total_bots++;
|
|
|
|
if (last_botdata)
|
|
last_botdata->next = botdata;
|
|
else // first bot
|
|
botinfo_list = botdata;
|
|
|
|
botdata->next = NULL;
|
|
}
|
|
}
|
|
else if (mode == 1) // teamplay data
|
|
{
|
|
if (!(bot_teams[total_teams] = ReadTeamData(&f)))
|
|
{
|
|
gi.error("\nError in BOTS.CFG: Invalid TEAM (#%i)\nEither re-install Eraser, or check your bots.cfg file for errors\n\n", total_teams);
|
|
break;
|
|
}
|
|
|
|
total_teams++;
|
|
|
|
if (total_teams == MAX_TEAMS)
|
|
{
|
|
gi.dprintf("Warning: MAX_TEAMS reached, unable to process all teams\n");
|
|
break;
|
|
}
|
|
}
|
|
else if (mode == 2) // view weapon models
|
|
{
|
|
ReadViewWeaponModel(&f);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
gi.dprintf("%i bots read.\n", total_bots);
|
|
if (teamplay->value)
|
|
gi.dprintf("%i teams read.\n", total_teams);
|
|
|
|
gi.dprintf("\n");
|
|
|
|
/* if (view_weapons->value && (num_view_weapons == 0))
|
|
{
|
|
gi.dprintf("WARNING: view_weapons enabled, but no [view weapon] section in bots.cfg\n You should re-install Eraser to restore the [view weapons] section.\n\n");
|
|
}*/
|
|
|
|
fclose (f);
|
|
|
|
ReadBotChat();
|
|
|
|
read_bot_cfg = true; // don't load again
|
|
}
|
|
|
|
qboolean ViewModelSupported(char *model)
|
|
{
|
|
int i;
|
|
|
|
for (i=0; i < num_view_weapons; i++)
|
|
{
|
|
if (!strcmp(view_weapon_models[i], model))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bot_info_t *GetBotData(char *botname)
|
|
{
|
|
bot_info_t *trav, *fallback=NULL;
|
|
qboolean done;
|
|
|
|
if (!botinfo_list)
|
|
{
|
|
gi.dprintf("No bots available!\n");
|
|
return NULL;
|
|
}
|
|
|
|
if (!botname)
|
|
{ // pick a random bot
|
|
int i, repeat_count=0;
|
|
|
|
if ((total_bots-1) == bot_count) // already using all bots!
|
|
return NULL;
|
|
|
|
i = (int) ceil(random() * ((total_bots-1) - bot_count));
|
|
|
|
// prevent Eraser from joining unless he is specifically asked for
|
|
trav = botinfo_list->next;
|
|
|
|
while (trav && (i > 0))
|
|
{
|
|
if (!trav->ingame_count) // not being used
|
|
i--;
|
|
|
|
if (i>0)
|
|
trav = trav->next;
|
|
}
|
|
|
|
done = false;
|
|
while (!done && trav)
|
|
{
|
|
done = true;
|
|
|
|
if (!fallback)
|
|
fallback = trav;
|
|
|
|
if (trav->ingame_count)
|
|
done = false;
|
|
/*else if (view_weapons->value)//ACRID FIXME HAD TO CO THIS FOR VIEWWEAPONS
|
|
{ // if this model isn't supported by view weapons, look for another one
|
|
char heldmodel[128];
|
|
int len;
|
|
|
|
strcpy(heldmodel, trav->skin);//fixme?
|
|
|
|
for(len = 0; heldmodel[len]; len++)
|
|
{
|
|
if(heldmodel[len] == '/')
|
|
{
|
|
heldmodel[len] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ViewModelSupported(heldmodel))
|
|
{ // model not supported by view_weapons
|
|
done = false;
|
|
}
|
|
}*/
|
|
|
|
if (!done)
|
|
{
|
|
if (!(trav = trav->next))
|
|
{
|
|
if (repeat_count < 2)
|
|
trav = botinfo_list->next;
|
|
|
|
repeat_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (trav)
|
|
{
|
|
return trav;
|
|
}
|
|
else if (fallback)
|
|
{
|
|
return fallback;
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("GetBotData(): random selection didn't work\n");
|
|
return NULL;
|
|
}
|
|
}
|
|
else // see if we can find the bot in the list
|
|
{
|
|
char name[128], checkname[128];
|
|
int i;
|
|
|
|
trav = botinfo_list;
|
|
|
|
// remove the team abbrev.
|
|
strcpy(checkname, botname);
|
|
i=0;
|
|
while (checkname[i] && (checkname[i] != '['))
|
|
i++;
|
|
|
|
if (checkname[i] == '[')
|
|
checkname[i] = 0;
|
|
|
|
// remove the team abbrev.
|
|
strcpy(name, trav->name);
|
|
i=0;
|
|
while (i<128 && name[i] && (name[i] != '['))
|
|
i++;
|
|
|
|
if (name[i] == '[')
|
|
name[i] = 0;
|
|
|
|
#ifdef _WIN32
|
|
while (trav && _stricmp(checkname, name)) // not case-sensitive
|
|
{
|
|
trav = trav->next;
|
|
#else
|
|
while (trav && strcasecmp(checkname, name)) // not case-sensitive
|
|
{
|
|
trav = trav->next;
|
|
#endif
|
|
if (trav)
|
|
{
|
|
// remove the team abbrev.
|
|
strcpy(name, trav->name);
|
|
i=0;
|
|
while (i<128 && name[i] && (name[i] != '['))
|
|
i++;
|
|
|
|
if (name[i] == '[')
|
|
name[i] = 0;
|
|
}
|
|
}
|
|
|
|
if (!trav)
|
|
{ // search through the teamplay bots
|
|
trav = teambot_list;
|
|
|
|
if (trav)
|
|
{
|
|
// remove the team abbrev.
|
|
strcpy(name, trav->name);
|
|
i=0;
|
|
while (i<128 && name[i] && (name[i] != '['))
|
|
i++;
|
|
|
|
if (name[i] == '[')
|
|
name[i] = 0;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
while (trav && _stricmp(checkname, name)) // not case-sensitive
|
|
{
|
|
trav = trav->next;
|
|
#else
|
|
while (trav && strcasecmp(checkname, name)) // not case-sensitive
|
|
{
|
|
trav = trav->next;
|
|
#endif
|
|
if (trav)
|
|
{
|
|
// remove the team abbrev.
|
|
strcpy(name, trav->name);
|
|
i=0;
|
|
while (i<128 && name[i] && (name[i] != '['))
|
|
i++;
|
|
|
|
if (name[i] == '[')
|
|
name[i] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return trav;
|
|
}
|
|
}
|
|
|
|
void FindVisibleItemsFromNode(edict_t *node)
|
|
{
|
|
int list;
|
|
int pi=0, besti=-1;
|
|
float bestdist=99999, thisdist;
|
|
vec3_t org;
|
|
edict_t *trav;
|
|
|
|
for (list=0; list<4; list++)
|
|
{
|
|
if (list==0)
|
|
trav = weapons_head;
|
|
else if (list==1)
|
|
trav = health_head;
|
|
else if (list==2)
|
|
trav = ammo_head;
|
|
else
|
|
trav = bonus_head;
|
|
|
|
while (trav)
|
|
{//lavacode
|
|
// make sure it's not in a dangerous position
|
|
VectorSubtract(trav->s.origin, tv(0,0,8), org);
|
|
if (!(gi.pointcontents(org) & (CONTENTS_LAVA | CONTENTS_SLIME)))
|
|
{
|
|
|
|
if ((thisdist = entdist(trav, node)) > 256)
|
|
goto next_ent;
|
|
|
|
// find the end of the item's visible node list
|
|
pi = 0;
|
|
while ((trav->paths[pi] > -1) && (pi < MAX_PATHS))
|
|
pi++;
|
|
|
|
if (pi == MAX_PATHS)
|
|
goto next_ent;
|
|
|
|
if (visible_box(node, trav) && CanReach(node, trav))
|
|
{
|
|
trav->paths[pi] = node->trail_index;
|
|
|
|
// see if this node is closer than the previous closest node
|
|
if (!trav->movetarget || (thisdist < entdist(trav, trav->movetarget)))
|
|
{
|
|
trav->movetarget = node;
|
|
}
|
|
}
|
|
}
|
|
|
|
next_ent:
|
|
|
|
trav = trav->node_target;
|
|
}
|
|
}
|
|
}
|
|
|
|
// skill_level ranges from 0 -> 3
|
|
void AdjustRatingsToSkill(edict_t *self)
|
|
{
|
|
self->bot_stats->accuracy = self->botdata->bot_stats.accuracy + (float)(self->skill_level - 1) * 2.5;
|
|
if (self->bot_stats->accuracy > 5)
|
|
self->bot_stats->accuracy = 5;
|
|
else if (self->bot_stats->accuracy < 1)
|
|
self->bot_stats->accuracy = 1;
|
|
|
|
self->bot_stats->combat = self->botdata->bot_stats.combat + (float)(self->skill_level - 1) * 2.5;
|
|
if (self->bot_stats->combat > 5)
|
|
self->bot_stats->combat = 5;
|
|
else if (self->bot_stats->combat < 1)
|
|
self->bot_stats->combat = 1;
|
|
|
|
self->bot_stats->aggr = self->botdata->bot_stats.aggr - (float)(self->skill_level - 1) * 2; // a good player, will make sure they stock up on armour/items before attacking
|
|
if (self->bot_stats->aggr > 5)
|
|
self->bot_stats->aggr = 5;
|
|
else if (self->bot_stats->aggr < 1)
|
|
self->bot_stats->aggr = 1;
|
|
}
|
|
//SHOWPATH PROBLE?
|
|
void target_laser_think (edict_t *self);
|
|
|
|
// draws a translucent line from spos to epos
|
|
edict_t *DrawLine(edict_t *owner, vec3_t spos, vec3_t epos)
|
|
{
|
|
edict_t *beam;
|
|
|
|
beam = G_Spawn();
|
|
|
|
beam->owner = owner;
|
|
beam->spawnflags = 1 | 4;
|
|
beam->classname = "path_beam";
|
|
|
|
beam->movetype = MOVETYPE_NONE;
|
|
beam->solid = SOLID_NOT;
|
|
beam->s.renderfx |= RF_BEAM|RF_TRANSLUCENT;
|
|
beam->s.modelindex = 1; //gi.modelindex ("models/objects/gibs/skull/tris.md2"); // must be non-zero
|
|
|
|
// set the beam diameter
|
|
beam->s.frame = 4;
|
|
// set the color (green)
|
|
beam->s.skinnum = 0xd0d1d2d3;
|
|
|
|
if (!beam->owner)
|
|
beam->owner = beam;
|
|
|
|
VectorSet (beam->mins, -8, -8, -8);
|
|
VectorSet (beam->maxs, 8, 8, 8);
|
|
|
|
beam->spawnflags |= 0x80000001;
|
|
beam->svflags &= ~SVF_NOCLIENT;
|
|
beam->flags |= FL_TEAMSLAVE;
|
|
|
|
VectorCopy(spos, beam->s.origin);
|
|
VectorCopy(epos, beam->s.old_origin);
|
|
|
|
VectorSubtract(epos, spos, beam->movedir);
|
|
// VectorNormalize2(beam->movedir, beam->movedir);
|
|
|
|
beam->dmg = 0;
|
|
beam->enemy = NULL;
|
|
|
|
beam->think = target_laser_think;
|
|
beam->nextthink = level.time + FRAMETIME;
|
|
beam->think(beam);
|
|
|
|
gi.linkentity (beam);
|
|
|
|
return beam;
|
|
}
|
|
|
|
// Do team checking, which is different for CTF and non-CTF modes
|
|
qboolean botOnSameTeam(edict_t *p1, edict_t *p2)
|
|
{
|
|
if (!ctf->value)
|
|
return (p1->client->team == p2->client->team);
|
|
else
|
|
return (p1->client->resp.ctf_team == p2->client->resp.ctf_team);
|
|
}
|
|
|
|
/*
|
|
============
|
|
TeamGroup
|
|
|
|
Issues a team grouping message, and alerts all bots accordingly
|
|
============
|
|
*/
|
|
void TeamGroup(edict_t *ent)
|
|
{
|
|
int i, chat_line, chat_type;
|
|
edict_t *closest, *trav;
|
|
float best_dist;
|
|
|
|
if (!ent->client->team && !ctf->value)
|
|
return;
|
|
|
|
if (ent->last_pain == level.time)
|
|
chat_type = CHAT_TEAMPLAY_HELP;
|
|
else
|
|
chat_type = CHAT_TEAMPLAY_GROUP;
|
|
|
|
chat_line = (int) (random() * (float) bot_chat_count[chat_type]);
|
|
|
|
// find closest item
|
|
closest = NULL;
|
|
best_dist = 512;
|
|
|
|
trav = weapons_head;
|
|
while (trav)
|
|
{
|
|
if (trav->item && (entdist(trav, ent) < best_dist))
|
|
{
|
|
closest = trav;
|
|
}
|
|
|
|
trav = trav->node_target;
|
|
}
|
|
|
|
if (!closest || (best_dist > 256))
|
|
{
|
|
trav = bonus_head;
|
|
while (trav)
|
|
{
|
|
if (trav->item && (trav->item->tag != ARMOR_SHARD) && (entdist(trav, ent) < best_dist))
|
|
{
|
|
closest = trav;
|
|
}
|
|
|
|
trav = trav->node_target;
|
|
}
|
|
}
|
|
|
|
if (!closest) // abort if no item is close by
|
|
return;
|
|
|
|
for (i=0; i<num_players; i++)
|
|
{
|
|
if ((players[i] == ent) || (botOnSameTeam(players[i], ent) && ((!players[i]->target_ent) || (random() < 0.4))))
|
|
{
|
|
if (!players[i]->bot_client)
|
|
{
|
|
safe_cprintf(players[i], PRINT_CHAT, "%s: ", ent->client->pers.netname);
|
|
safe_cprintf(players[i], PRINT_CHAT, bot_chat_text[chat_type][chat_line], closest->item->pickup_name);
|
|
safe_cprintf(players[i], PRINT_CHAT, "\n");
|
|
}
|
|
else if (!players[i]->target_ent)
|
|
{ // set bot to go straight to this player
|
|
players[i]->target_ent = ent;
|
|
}
|
|
}
|
|
}
|
|
|
|
ent->group_pausetime = level.time + 10 + 5 * random();
|
|
|
|
if (ent->client->team)
|
|
ent->client->team->last_grouping = level.time + 5 + (random() * 10);
|
|
}
|
|
|
|
// tells all following bots to disperse
|
|
void TeamDisperse(edict_t *self)
|
|
{
|
|
int i, count=0;
|
|
|
|
for (i=0; i<num_players; i++)
|
|
{
|
|
if (players[i]->target_ent == self)
|
|
{
|
|
players[i]->target_ent = NULL;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count)
|
|
{
|
|
safe_cprintf(self, PRINT_CHAT, "%s: ", self->client->pers.netname);
|
|
safe_cprintf(self, PRINT_CHAT, "all units disperse!\n");
|
|
}
|
|
}
|
|
|
|
void BotGreeting(edict_t *chat)
|
|
{
|
|
int i;
|
|
|
|
if (!bot_chat->value)
|
|
return;
|
|
|
|
i = (int) (random() * (float) bot_chat_count[CHAT_GREETINGS]);
|
|
|
|
my_bprintf(PRINT_CHAT, "%s: ", chat->owner->client->pers.netname);
|
|
my_bprintf(PRINT_CHAT, bot_chat_text[CHAT_GREETINGS][i]);
|
|
my_bprintf(PRINT_CHAT, "\n");
|
|
|
|
G_FreeEdict(chat);
|
|
}
|
|
|
|
void BotComeback(edict_t *self)
|
|
{
|
|
int i;
|
|
|
|
if (!bot_chat->value)
|
|
return;
|
|
|
|
i = (int) (random() * (float) bot_chat_count[CHAT_COMEBACKS]);
|
|
|
|
my_bprintf(PRINT_CHAT, "%s: ", self->owner->client->pers.netname);
|
|
my_bprintf(PRINT_CHAT, bot_chat_text[CHAT_COMEBACKS][i], self->enemy->client->pers.netname);
|
|
my_bprintf(PRINT_CHAT, "\n");
|
|
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void BotInsultStart(edict_t *self)
|
|
{
|
|
// insult?
|
|
if (fabs(self->owner->client->resp.score - self->enemy->client->resp.score) < 5)
|
|
{ // general insult
|
|
if (last_bot_chat[CHAT_INSULTS_GENERAL] < (level.time - 5))
|
|
BotInsult(self->owner, self->enemy, CHAT_INSULTS_GENERAL);
|
|
}
|
|
else
|
|
{
|
|
if (self->owner->client->resp.score > self->enemy->client->resp.score)
|
|
{ // kickin ass
|
|
if (last_bot_chat[CHAT_INSULTS_KICKASS] < (level.time - 5))
|
|
BotInsult(self->owner, self->enemy, CHAT_INSULTS_KICKASS);
|
|
}
|
|
else
|
|
{
|
|
if (last_bot_chat[CHAT_INSULTS_LOSING] < (level.time - 5))
|
|
BotInsult(self->owner, self->enemy, CHAT_INSULTS_LOSING);
|
|
}
|
|
}
|
|
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void BotInsult(edict_t *self, edict_t *enemy, int chat_type)
|
|
{
|
|
int i;
|
|
|
|
if (!bot_chat->value)
|
|
return;
|
|
|
|
i = (int) (random() * (float) bot_chat_count[chat_type]);
|
|
|
|
my_bprintf(PRINT_CHAT, "%s: ", self->client->pers.netname);
|
|
my_bprintf(PRINT_CHAT, bot_chat_text[chat_type][i], enemy->client->pers.netname);
|
|
my_bprintf(PRINT_CHAT, "\n");
|
|
|
|
last_bot_chat[chat_type] = level.time;
|
|
|
|
if (enemy->bot_client && (random() < 0.3) && (last_bot_chat[CHAT_COMEBACKS] < (level.time - 3)))
|
|
{
|
|
edict_t *comeback;
|
|
|
|
comeback = G_Spawn();
|
|
comeback->think = BotComeback;
|
|
comeback->nextthink = level.time + 2 + random();
|
|
comeback->enemy = self;
|
|
comeback->owner = enemy;
|
|
|
|
last_bot_chat[CHAT_COMEBACKS] = level.time + 5;
|
|
}
|
|
}
|
|
|
|
qboolean SameTeam(edict_t *plyr1, edict_t *plyr2)
|
|
{ // returns true if the 2 players are on the same team
|
|
if (ctf->value)
|
|
{
|
|
return (plyr1->client->resp.ctf_team == plyr2->client->resp.ctf_team);
|
|
}
|
|
else
|
|
{
|
|
// neutral players attack anyone
|
|
if (!plyr1->client->team || !plyr2->client->team)
|
|
return false;
|
|
|
|
return (plyr1->client->team == plyr2->client->team);
|
|
}
|
|
}
|
|
|
|
float HomeFlagDist(edict_t *self)
|
|
{
|
|
edict_t *flag;
|
|
|
|
if (self->client->resp.ctf_team == CTF_TEAM1)
|
|
flag = flag1_ent;
|
|
else
|
|
flag = flag2_ent;
|
|
|
|
return entdist(self, flag);
|
|
}
|
|
|
|
qboolean CarryingFlag(edict_t *ent)
|
|
{ // return true if ent is carrying the enemy flag
|
|
/*
|
|
if (!ctf->value)
|
|
return false;
|
|
if (!ent->client)
|
|
return false;
|
|
|
|
if (ent->client->resp.ctf_team == CTF_TEAM1)
|
|
return ent->client->pers.inventory[ITEM_INDEX(flag2_item)];
|
|
else
|
|
return ent->client->pers.inventory[ITEM_INDEX(flag1_item)];
|
|
*/
|
|
return (ent->s.effects & (EF_FLAG1|EF_FLAG2));
|
|
}
|