mirror of
https://github.com/DrBeef/ioq3quest.git
synced 2024-11-27 06:13:01 +00:00
2292bf5bb2
If a bot is accompanying someone before map change or restart, the bot would continue accompanying them but press up against them and orbit around them. This is caused by the bot's formation distance being 0. Save the formation distance so they maintain proper distance and do not orbit around the player.
1722 lines
45 KiB
C
1722 lines
45 KiB
C
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code is free software; you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code is distributed in the hope that it will be
|
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
//
|
|
|
|
/*****************************************************************************
|
|
* name: ai_main.c
|
|
*
|
|
* desc: Quake3 bot AI
|
|
*
|
|
* $Archive: /MissionPack/code/game/ai_main.c $
|
|
*
|
|
*****************************************************************************/
|
|
|
|
|
|
#include "g_local.h"
|
|
#include "../qcommon/q_shared.h"
|
|
#include "../botlib/botlib.h" //bot lib interface
|
|
#include "../botlib/be_aas.h"
|
|
#include "../botlib/be_ea.h"
|
|
#include "../botlib/be_ai_char.h"
|
|
#include "../botlib/be_ai_chat.h"
|
|
#include "../botlib/be_ai_gen.h"
|
|
#include "../botlib/be_ai_goal.h"
|
|
#include "../botlib/be_ai_move.h"
|
|
#include "../botlib/be_ai_weap.h"
|
|
//
|
|
#include "ai_main.h"
|
|
#include "ai_dmq3.h"
|
|
#include "ai_chat.h"
|
|
#include "ai_cmd.h"
|
|
#include "ai_dmnet.h"
|
|
#include "ai_vcmd.h"
|
|
|
|
//
|
|
#include "chars.h"
|
|
#include "inv.h"
|
|
#include "syn.h"
|
|
|
|
#ifndef MAX_PATH
|
|
#define MAX_PATH 144
|
|
#endif
|
|
|
|
|
|
//bot states
|
|
bot_state_t *botstates[MAX_CLIENTS];
|
|
//number of bots
|
|
int numbots;
|
|
//floating point time
|
|
float floattime;
|
|
//time to do a regular update
|
|
float regularupdate_time;
|
|
//
|
|
int bot_interbreed;
|
|
int bot_interbreedmatchcount;
|
|
//
|
|
vmCvar_t bot_thinktime;
|
|
vmCvar_t bot_memorydump;
|
|
vmCvar_t bot_saveroutingcache;
|
|
vmCvar_t bot_pause;
|
|
vmCvar_t bot_report;
|
|
vmCvar_t bot_testsolid;
|
|
vmCvar_t bot_testclusters;
|
|
vmCvar_t bot_developer;
|
|
vmCvar_t bot_interbreedchar;
|
|
vmCvar_t bot_interbreedbots;
|
|
vmCvar_t bot_interbreedcycle;
|
|
vmCvar_t bot_interbreedwrite;
|
|
|
|
|
|
void ExitLevel( void );
|
|
|
|
|
|
/*
|
|
==================
|
|
BotAI_Print
|
|
==================
|
|
*/
|
|
void QDECL BotAI_Print(int type, char *fmt, ...) {
|
|
char str[2048];
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
Q_vsnprintf(str, sizeof(str), fmt, ap);
|
|
va_end(ap);
|
|
|
|
switch(type) {
|
|
case PRT_MESSAGE: {
|
|
G_Printf("%s", str);
|
|
break;
|
|
}
|
|
case PRT_WARNING: {
|
|
G_Printf( S_COLOR_YELLOW "Warning: %s", str );
|
|
break;
|
|
}
|
|
case PRT_ERROR: {
|
|
G_Printf( S_COLOR_RED "Error: %s", str );
|
|
break;
|
|
}
|
|
case PRT_FATAL: {
|
|
G_Printf( S_COLOR_RED "Fatal: %s", str );
|
|
break;
|
|
}
|
|
case PRT_EXIT: {
|
|
G_Error( S_COLOR_RED "Exit: %s", str );
|
|
break;
|
|
}
|
|
default: {
|
|
G_Printf( "unknown print type\n" );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
BotAI_Trace
|
|
==================
|
|
*/
|
|
void BotAI_Trace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end, int passent, int contentmask) {
|
|
trace_t trace;
|
|
|
|
trap_Trace(&trace, start, mins, maxs, end, passent, contentmask);
|
|
//copy the trace information
|
|
bsptrace->allsolid = trace.allsolid;
|
|
bsptrace->startsolid = trace.startsolid;
|
|
bsptrace->fraction = trace.fraction;
|
|
VectorCopy(trace.endpos, bsptrace->endpos);
|
|
bsptrace->plane.dist = trace.plane.dist;
|
|
VectorCopy(trace.plane.normal, bsptrace->plane.normal);
|
|
bsptrace->plane.signbits = trace.plane.signbits;
|
|
bsptrace->plane.type = trace.plane.type;
|
|
bsptrace->surface.value = 0;
|
|
bsptrace->surface.flags = trace.surfaceFlags;
|
|
bsptrace->ent = trace.entityNum;
|
|
bsptrace->exp_dist = 0;
|
|
bsptrace->sidenum = 0;
|
|
bsptrace->contents = 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAI_GetClientState
|
|
==================
|
|
*/
|
|
int BotAI_GetClientState( int clientNum, playerState_t *state ) {
|
|
gentity_t *ent;
|
|
|
|
ent = &g_entities[clientNum];
|
|
if ( !ent->inuse ) {
|
|
return qfalse;
|
|
}
|
|
if ( !ent->client ) {
|
|
return qfalse;
|
|
}
|
|
|
|
memcpy( state, &ent->client->ps, sizeof(playerState_t) );
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAI_GetEntityState
|
|
==================
|
|
*/
|
|
int BotAI_GetEntityState( int entityNum, entityState_t *state ) {
|
|
gentity_t *ent;
|
|
|
|
ent = &g_entities[entityNum];
|
|
memset( state, 0, sizeof(entityState_t) );
|
|
if (!ent->inuse) return qfalse;
|
|
if (!ent->r.linked) return qfalse;
|
|
if (ent->r.svFlags & SVF_NOCLIENT) return qfalse;
|
|
memcpy( state, &ent->s, sizeof(entityState_t) );
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAI_GetSnapshotEntity
|
|
==================
|
|
*/
|
|
int BotAI_GetSnapshotEntity( int clientNum, int sequence, entityState_t *state ) {
|
|
int entNum;
|
|
|
|
entNum = trap_BotGetSnapshotEntity( clientNum, sequence );
|
|
if ( entNum == -1 ) {
|
|
memset(state, 0, sizeof(entityState_t));
|
|
return -1;
|
|
}
|
|
|
|
BotAI_GetEntityState( entNum, state );
|
|
|
|
return sequence + 1;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotAI_BotInitialChat
|
|
==================
|
|
*/
|
|
void QDECL BotAI_BotInitialChat( bot_state_t *bs, char *type, ... ) {
|
|
int i, mcontext;
|
|
va_list ap;
|
|
char *p;
|
|
char *vars[MAX_MATCHVARIABLES];
|
|
|
|
memset(vars, 0, sizeof(vars));
|
|
va_start(ap, type);
|
|
p = va_arg(ap, char *);
|
|
for (i = 0; i < MAX_MATCHVARIABLES; i++) {
|
|
if( !p ) {
|
|
break;
|
|
}
|
|
vars[i] = p;
|
|
p = va_arg(ap, char *);
|
|
}
|
|
va_end(ap);
|
|
|
|
mcontext = BotSynonymContext(bs);
|
|
|
|
trap_BotInitialChat( bs->cs, type, mcontext, vars[0], vars[1], vars[2], vars[3], vars[4], vars[5], vars[6], vars[7] );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
BotTestAAS
|
|
==================
|
|
*/
|
|
void BotTestAAS(vec3_t origin) {
|
|
int areanum;
|
|
aas_areainfo_t info;
|
|
|
|
trap_Cvar_Update(&bot_testsolid);
|
|
trap_Cvar_Update(&bot_testclusters);
|
|
if (bot_testsolid.integer) {
|
|
if (!trap_AAS_Initialized()) return;
|
|
areanum = BotPointAreaNum(origin);
|
|
if (areanum) BotAI_Print(PRT_MESSAGE, "\remtpy area");
|
|
else BotAI_Print(PRT_MESSAGE, "\r^1SOLID area");
|
|
}
|
|
else if (bot_testclusters.integer) {
|
|
if (!trap_AAS_Initialized()) return;
|
|
areanum = BotPointAreaNum(origin);
|
|
if (!areanum)
|
|
BotAI_Print(PRT_MESSAGE, "\r^1Solid! ");
|
|
else {
|
|
trap_AAS_AreaInfo(areanum, &info);
|
|
BotAI_Print(PRT_MESSAGE, "\rarea %d, cluster %d ", areanum, info.cluster);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotReportStatus
|
|
==================
|
|
*/
|
|
void BotReportStatus(bot_state_t *bs) {
|
|
char goalname[MAX_MESSAGE_SIZE];
|
|
char netname[MAX_MESSAGE_SIZE];
|
|
char *leader, flagstatus[32];
|
|
//
|
|
ClientName(bs->client, netname, sizeof(netname));
|
|
if (Q_stricmp(netname, bs->teamleader) == 0) leader = "L";
|
|
else leader = " ";
|
|
|
|
strcpy(flagstatus, " ");
|
|
if (gametype == GT_CTF) {
|
|
if (BotCTFCarryingFlag(bs)) {
|
|
if (BotTeam(bs) == TEAM_RED) strcpy(flagstatus, S_COLOR_RED"F ");
|
|
else strcpy(flagstatus, S_COLOR_BLUE"F ");
|
|
}
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (Bot1FCTFCarryingFlag(bs)) {
|
|
if (BotTeam(bs) == TEAM_RED) strcpy(flagstatus, S_COLOR_RED"F ");
|
|
else strcpy(flagstatus, S_COLOR_BLUE"F ");
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (BotHarvesterCarryingCubes(bs)) {
|
|
if (BotTeam(bs) == TEAM_RED) Com_sprintf(flagstatus, sizeof(flagstatus), S_COLOR_RED"%2d", bs->inventory[INVENTORY_REDCUBE]);
|
|
else Com_sprintf(flagstatus, sizeof(flagstatus), S_COLOR_BLUE"%2d", bs->inventory[INVENTORY_BLUECUBE]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
switch(bs->ltgtype) {
|
|
case LTG_TEAMHELP:
|
|
{
|
|
EasyClientName(bs->teammate, goalname, sizeof(goalname));
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: helping %s\n", netname, leader, flagstatus, goalname);
|
|
break;
|
|
}
|
|
case LTG_TEAMACCOMPANY:
|
|
{
|
|
EasyClientName(bs->teammate, goalname, sizeof(goalname));
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: accompanying %s\n", netname, leader, flagstatus, goalname);
|
|
break;
|
|
}
|
|
case LTG_DEFENDKEYAREA:
|
|
{
|
|
trap_BotGoalName(bs->teamgoal.number, goalname, sizeof(goalname));
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: defending %s\n", netname, leader, flagstatus, goalname);
|
|
break;
|
|
}
|
|
case LTG_GETITEM:
|
|
{
|
|
trap_BotGoalName(bs->teamgoal.number, goalname, sizeof(goalname));
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: getting item %s\n", netname, leader, flagstatus, goalname);
|
|
break;
|
|
}
|
|
case LTG_KILL:
|
|
{
|
|
ClientName(bs->teamgoal.entitynum, goalname, sizeof(goalname));
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: killing %s\n", netname, leader, flagstatus, goalname);
|
|
break;
|
|
}
|
|
case LTG_CAMP:
|
|
case LTG_CAMPORDER:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: camping\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_PATROL:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: patrolling\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_GETFLAG:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: capturing flag\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_RUSHBASE:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: rushing base\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_RETURNFLAG:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: returning flag\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_ATTACKENEMYBASE:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: attacking the enemy base\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
case LTG_HARVEST:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: harvesting\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
BotAI_Print(PRT_MESSAGE, "%-20s%s%s: roaming\n", netname, leader, flagstatus);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotTeamplayReport
|
|
==================
|
|
*/
|
|
void BotTeamplayReport(void) {
|
|
int i;
|
|
char buf[MAX_INFO_STRING];
|
|
|
|
BotAI_Print(PRT_MESSAGE, S_COLOR_RED"RED\n");
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
//
|
|
if ( !botstates[i] || !botstates[i]->inuse ) continue;
|
|
//
|
|
trap_GetConfigstring(CS_PLAYERS+i, buf, sizeof(buf));
|
|
//if no config string or no name
|
|
if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n"))) continue;
|
|
//skip spectators
|
|
if (atoi(Info_ValueForKey(buf, "t")) == TEAM_RED) {
|
|
BotReportStatus(botstates[i]);
|
|
}
|
|
}
|
|
BotAI_Print(PRT_MESSAGE, S_COLOR_BLUE"BLUE\n");
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
//
|
|
if ( !botstates[i] || !botstates[i]->inuse ) continue;
|
|
//
|
|
trap_GetConfigstring(CS_PLAYERS+i, buf, sizeof(buf));
|
|
//if no config string or no name
|
|
if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n"))) continue;
|
|
//skip spectators
|
|
if (atoi(Info_ValueForKey(buf, "t")) == TEAM_BLUE) {
|
|
BotReportStatus(botstates[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotSetInfoConfigString
|
|
==================
|
|
*/
|
|
void BotSetInfoConfigString(bot_state_t *bs) {
|
|
char goalname[MAX_MESSAGE_SIZE];
|
|
char netname[MAX_MESSAGE_SIZE];
|
|
char action[MAX_MESSAGE_SIZE];
|
|
char *leader, carrying[32], *cs;
|
|
bot_goal_t goal;
|
|
//
|
|
ClientName(bs->client, netname, sizeof(netname));
|
|
if (Q_stricmp(netname, bs->teamleader) == 0) leader = "L";
|
|
else leader = " ";
|
|
|
|
strcpy(carrying, " ");
|
|
if (gametype == GT_CTF) {
|
|
if (BotCTFCarryingFlag(bs)) {
|
|
strcpy(carrying, "F ");
|
|
}
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (gametype == GT_1FCTF) {
|
|
if (Bot1FCTFCarryingFlag(bs)) {
|
|
strcpy(carrying, "F ");
|
|
}
|
|
}
|
|
else if (gametype == GT_HARVESTER) {
|
|
if (BotHarvesterCarryingCubes(bs)) {
|
|
if (BotTeam(bs) == TEAM_RED) Com_sprintf(carrying, sizeof(carrying), "%2d", bs->inventory[INVENTORY_REDCUBE]);
|
|
else Com_sprintf(carrying, sizeof(carrying), "%2d", bs->inventory[INVENTORY_BLUECUBE]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
switch(bs->ltgtype) {
|
|
case LTG_TEAMHELP:
|
|
{
|
|
EasyClientName(bs->teammate, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "helping %s", goalname);
|
|
break;
|
|
}
|
|
case LTG_TEAMACCOMPANY:
|
|
{
|
|
EasyClientName(bs->teammate, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "accompanying %s", goalname);
|
|
break;
|
|
}
|
|
case LTG_DEFENDKEYAREA:
|
|
{
|
|
trap_BotGoalName(bs->teamgoal.number, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "defending %s", goalname);
|
|
break;
|
|
}
|
|
case LTG_GETITEM:
|
|
{
|
|
trap_BotGoalName(bs->teamgoal.number, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "getting item %s", goalname);
|
|
break;
|
|
}
|
|
case LTG_KILL:
|
|
{
|
|
ClientName(bs->teamgoal.entitynum, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "killing %s", goalname);
|
|
break;
|
|
}
|
|
case LTG_CAMP:
|
|
case LTG_CAMPORDER:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "camping");
|
|
break;
|
|
}
|
|
case LTG_PATROL:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "patrolling");
|
|
break;
|
|
}
|
|
case LTG_GETFLAG:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "capturing flag");
|
|
break;
|
|
}
|
|
case LTG_RUSHBASE:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "rushing base");
|
|
break;
|
|
}
|
|
case LTG_RETURNFLAG:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "returning flag");
|
|
break;
|
|
}
|
|
case LTG_ATTACKENEMYBASE:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "attacking the enemy base");
|
|
break;
|
|
}
|
|
case LTG_HARVEST:
|
|
{
|
|
Com_sprintf(action, sizeof(action), "harvesting");
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
trap_BotGetTopGoal(bs->gs, &goal);
|
|
trap_BotGoalName(goal.number, goalname, sizeof(goalname));
|
|
Com_sprintf(action, sizeof(action), "roaming %s", goalname);
|
|
break;
|
|
}
|
|
}
|
|
cs = va("l\\%s\\c\\%s\\a\\%s",
|
|
leader,
|
|
carrying,
|
|
action);
|
|
trap_SetConfigstring (CS_BOTINFO + bs->client, cs);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotUpdateInfoConfigStrings
|
|
==============
|
|
*/
|
|
void BotUpdateInfoConfigStrings(void) {
|
|
int i;
|
|
char buf[MAX_INFO_STRING];
|
|
|
|
for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
|
|
//
|
|
if ( !botstates[i] || !botstates[i]->inuse )
|
|
continue;
|
|
//
|
|
trap_GetConfigstring(CS_PLAYERS+i, buf, sizeof(buf));
|
|
//if no config string or no name
|
|
if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n")))
|
|
continue;
|
|
BotSetInfoConfigString(botstates[i]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotInterbreedBots
|
|
==============
|
|
*/
|
|
void BotInterbreedBots(void) {
|
|
float ranks[MAX_CLIENTS];
|
|
int parent1, parent2, child;
|
|
int i;
|
|
|
|
// get rankings for all the bots
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if ( botstates[i] && botstates[i]->inuse ) {
|
|
ranks[i] = botstates[i]->num_kills * 2 - botstates[i]->num_deaths;
|
|
}
|
|
else {
|
|
ranks[i] = -1;
|
|
}
|
|
}
|
|
|
|
if (trap_GeneticParentsAndChildSelection(MAX_CLIENTS, ranks, &parent1, &parent2, &child)) {
|
|
trap_BotInterbreedGoalFuzzyLogic(botstates[parent1]->gs, botstates[parent2]->gs, botstates[child]->gs);
|
|
trap_BotMutateGoalFuzzyLogic(botstates[child]->gs, 1);
|
|
}
|
|
// reset the kills and deaths
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if (botstates[i] && botstates[i]->inuse) {
|
|
botstates[i]->num_kills = 0;
|
|
botstates[i]->num_deaths = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotWriteInterbreeded
|
|
==============
|
|
*/
|
|
void BotWriteInterbreeded(char *filename) {
|
|
float rank, bestrank;
|
|
int i, bestbot;
|
|
|
|
bestrank = 0;
|
|
bestbot = -1;
|
|
// get the best bot
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if ( botstates[i] && botstates[i]->inuse ) {
|
|
rank = botstates[i]->num_kills * 2 - botstates[i]->num_deaths;
|
|
}
|
|
else {
|
|
rank = -1;
|
|
}
|
|
if (rank > bestrank) {
|
|
bestrank = rank;
|
|
bestbot = i;
|
|
}
|
|
}
|
|
if (bestbot >= 0) {
|
|
//write out the new goal fuzzy logic
|
|
trap_BotSaveGoalFuzzyLogic(botstates[bestbot]->gs, filename);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotInterbreedEndMatch
|
|
|
|
add link back into ExitLevel?
|
|
==============
|
|
*/
|
|
void BotInterbreedEndMatch(void) {
|
|
|
|
if (!bot_interbreed) return;
|
|
bot_interbreedmatchcount++;
|
|
if (bot_interbreedmatchcount >= bot_interbreedcycle.integer) {
|
|
bot_interbreedmatchcount = 0;
|
|
//
|
|
trap_Cvar_Update(&bot_interbreedwrite);
|
|
if (strlen(bot_interbreedwrite.string)) {
|
|
BotWriteInterbreeded(bot_interbreedwrite.string);
|
|
trap_Cvar_Set("bot_interbreedwrite", "");
|
|
}
|
|
BotInterbreedBots();
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotInterbreeding
|
|
==============
|
|
*/
|
|
void BotInterbreeding(void) {
|
|
int i;
|
|
|
|
trap_Cvar_Update(&bot_interbreedchar);
|
|
if (!strlen(bot_interbreedchar.string)) return;
|
|
//make sure we are in tournament mode
|
|
if (gametype != GT_TOURNAMENT) {
|
|
trap_Cvar_Set("g_gametype", va("%d", GT_TOURNAMENT));
|
|
ExitLevel();
|
|
return;
|
|
}
|
|
//shutdown all the bots
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if (botstates[i] && botstates[i]->inuse) {
|
|
BotAIShutdownClient(botstates[i]->client, qfalse);
|
|
}
|
|
}
|
|
//make sure all item weight configs are reloaded and Not shared
|
|
trap_BotLibVarSet("bot_reloadcharacters", "1");
|
|
//add a number of bots using the desired bot character
|
|
for (i = 0; i < bot_interbreedbots.integer; i++) {
|
|
trap_SendConsoleCommand( EXEC_INSERT, va("addbot %s 4 free %i %s%d\n",
|
|
bot_interbreedchar.string, i * 50, bot_interbreedchar.string, i) );
|
|
}
|
|
//
|
|
trap_Cvar_Set("bot_interbreedchar", "");
|
|
bot_interbreed = qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotEntityInfo
|
|
==============
|
|
*/
|
|
void BotEntityInfo(int entnum, aas_entityinfo_t *info) {
|
|
trap_AAS_EntityInfo(entnum, info);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
NumBots
|
|
==============
|
|
*/
|
|
int NumBots(void) {
|
|
return numbots;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotTeamLeader
|
|
==============
|
|
*/
|
|
int BotTeamLeader(bot_state_t *bs) {
|
|
int leader;
|
|
|
|
leader = ClientFromName(bs->teamleader);
|
|
if (leader < 0) return qfalse;
|
|
if (!botstates[leader] || !botstates[leader]->inuse) return qfalse;
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
AngleDifference
|
|
==============
|
|
*/
|
|
float AngleDifference(float ang1, float ang2) {
|
|
float diff;
|
|
|
|
diff = ang1 - ang2;
|
|
if (ang1 > ang2) {
|
|
if (diff > 180.0) diff -= 360.0;
|
|
}
|
|
else {
|
|
if (diff < -180.0) diff += 360.0;
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotChangeViewAngle
|
|
==============
|
|
*/
|
|
float BotChangeViewAngle(float angle, float ideal_angle, float speed) {
|
|
float move;
|
|
|
|
angle = AngleMod(angle);
|
|
ideal_angle = AngleMod(ideal_angle);
|
|
if (angle == ideal_angle) return angle;
|
|
move = ideal_angle - angle;
|
|
if (ideal_angle > angle) {
|
|
if (move > 180.0) move -= 360.0;
|
|
}
|
|
else {
|
|
if (move < -180.0) move += 360.0;
|
|
}
|
|
if (move > 0) {
|
|
if (move > speed) move = speed;
|
|
}
|
|
else {
|
|
if (move < -speed) move = -speed;
|
|
}
|
|
return AngleMod(angle + move);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotChangeViewAngles
|
|
==============
|
|
*/
|
|
void BotChangeViewAngles(bot_state_t *bs, float thinktime) {
|
|
float diff, factor, maxchange, anglespeed, disired_speed;
|
|
int i;
|
|
|
|
if (bs->ideal_viewangles[PITCH] > 180) bs->ideal_viewangles[PITCH] -= 360;
|
|
//
|
|
if (bs->enemy >= 0) {
|
|
factor = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1);
|
|
maxchange = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_MAXCHANGE, 1, 1800);
|
|
}
|
|
else {
|
|
factor = 0.05f;
|
|
maxchange = 360;
|
|
}
|
|
if (maxchange < 240) maxchange = 240;
|
|
maxchange *= thinktime;
|
|
for (i = 0; i < 2; i++) {
|
|
//
|
|
if (bot_challenge.integer) {
|
|
//smooth slowdown view model
|
|
diff = abs(AngleDifference(bs->viewangles[i], bs->ideal_viewangles[i]));
|
|
anglespeed = diff * factor;
|
|
if (anglespeed > maxchange) anglespeed = maxchange;
|
|
bs->viewangles[i] = BotChangeViewAngle(bs->viewangles[i],
|
|
bs->ideal_viewangles[i], anglespeed);
|
|
}
|
|
else {
|
|
//over reaction view model
|
|
bs->viewangles[i] = AngleMod(bs->viewangles[i]);
|
|
bs->ideal_viewangles[i] = AngleMod(bs->ideal_viewangles[i]);
|
|
diff = AngleDifference(bs->viewangles[i], bs->ideal_viewangles[i]);
|
|
disired_speed = diff * factor;
|
|
bs->viewanglespeed[i] += (bs->viewanglespeed[i] - disired_speed);
|
|
if (bs->viewanglespeed[i] > 180) bs->viewanglespeed[i] = maxchange;
|
|
if (bs->viewanglespeed[i] < -180) bs->viewanglespeed[i] = -maxchange;
|
|
anglespeed = bs->viewanglespeed[i];
|
|
if (anglespeed > maxchange) anglespeed = maxchange;
|
|
if (anglespeed < -maxchange) anglespeed = -maxchange;
|
|
bs->viewangles[i] += anglespeed;
|
|
bs->viewangles[i] = AngleMod(bs->viewangles[i]);
|
|
//demping
|
|
bs->viewanglespeed[i] *= 0.45 * (1 - factor);
|
|
}
|
|
//BotAI_Print(PRT_MESSAGE, "ideal_angles %f %f\n", bs->ideal_viewangles[0], bs->ideal_viewangles[1], bs->ideal_viewangles[2]);`
|
|
//bs->viewangles[i] = bs->ideal_viewangles[i];
|
|
}
|
|
//bs->viewangles[PITCH] = 0;
|
|
if (bs->viewangles[PITCH] > 180) bs->viewangles[PITCH] -= 360;
|
|
//elementary action: view
|
|
trap_EA_View(bs->client, bs->viewangles);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotInputToUserCommand
|
|
==============
|
|
*/
|
|
void BotInputToUserCommand(bot_input_t *bi, usercmd_t *ucmd, int delta_angles[3], int time) {
|
|
vec3_t angles, forward, right;
|
|
short temp;
|
|
int j;
|
|
float f, r, u, m;
|
|
|
|
//clear the whole structure
|
|
memset(ucmd, 0, sizeof(usercmd_t));
|
|
//the duration for the user command in milli seconds
|
|
ucmd->serverTime = time;
|
|
//
|
|
if (bi->actionflags & ACTION_DELAYEDJUMP) {
|
|
bi->actionflags |= ACTION_JUMP;
|
|
bi->actionflags &= ~ACTION_DELAYEDJUMP;
|
|
}
|
|
//set the buttons
|
|
if (bi->actionflags & ACTION_RESPAWN) ucmd->buttons = BUTTON_ATTACK;
|
|
if (bi->actionflags & ACTION_ATTACK) ucmd->buttons |= BUTTON_ATTACK;
|
|
if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK;
|
|
if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE;
|
|
if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE;
|
|
if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_WALKING;
|
|
if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE;
|
|
if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE;
|
|
if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG;
|
|
if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE;
|
|
if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL;
|
|
if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME;
|
|
//
|
|
ucmd->weapon = bi->weapon;
|
|
//set the view angles
|
|
//NOTE: the ucmd->angles are the angles WITHOUT the delta angles
|
|
ucmd->angles[PITCH] = ANGLE2SHORT(bi->viewangles[PITCH]);
|
|
ucmd->angles[YAW] = ANGLE2SHORT(bi->viewangles[YAW]);
|
|
ucmd->angles[ROLL] = ANGLE2SHORT(bi->viewangles[ROLL]);
|
|
//subtract the delta angles
|
|
for (j = 0; j < 3; j++) {
|
|
temp = ucmd->angles[j] - delta_angles[j];
|
|
/*NOTE: disabled because temp should be mod first
|
|
if ( j == PITCH ) {
|
|
// don't let the player look up or down more than 90 degrees
|
|
if ( temp > 16000 ) temp = 16000;
|
|
else if ( temp < -16000 ) temp = -16000;
|
|
}
|
|
*/
|
|
ucmd->angles[j] = temp;
|
|
}
|
|
//NOTE: movement is relative to the REAL view angles
|
|
//get the horizontal forward and right vector
|
|
//get the pitch in the range [-180, 180]
|
|
if (bi->dir[2]) angles[PITCH] = bi->viewangles[PITCH];
|
|
else angles[PITCH] = 0;
|
|
angles[YAW] = bi->viewangles[YAW];
|
|
angles[ROLL] = 0;
|
|
AngleVectors(angles, forward, right, NULL);
|
|
//bot input speed is in the range [0, 400]
|
|
bi->speed = bi->speed * 127 / 400;
|
|
//set the view independent movement
|
|
f = DotProduct(forward, bi->dir);
|
|
r = DotProduct(right, bi->dir);
|
|
u = abs(forward[2]) * bi->dir[2];
|
|
m = fabs(f);
|
|
|
|
if (fabs(r) > m) {
|
|
m = fabs(r);
|
|
}
|
|
|
|
if (fabs(u) > m) {
|
|
m = fabs(u);
|
|
}
|
|
|
|
if (m > 0) {
|
|
f *= bi->speed / m;
|
|
r *= bi->speed / m;
|
|
u *= bi->speed / m;
|
|
}
|
|
|
|
ucmd->forwardmove = f;
|
|
ucmd->rightmove = r;
|
|
ucmd->upmove = u;
|
|
|
|
if (bi->actionflags & ACTION_MOVEFORWARD) ucmd->forwardmove = 127;
|
|
if (bi->actionflags & ACTION_MOVEBACK) ucmd->forwardmove = -127;
|
|
if (bi->actionflags & ACTION_MOVELEFT) ucmd->rightmove = -127;
|
|
if (bi->actionflags & ACTION_MOVERIGHT) ucmd->rightmove = 127;
|
|
//jump/moveup
|
|
if (bi->actionflags & ACTION_JUMP) ucmd->upmove = 127;
|
|
//crouch/movedown
|
|
if (bi->actionflags & ACTION_CROUCH) ucmd->upmove = -127;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotUpdateInput
|
|
==============
|
|
*/
|
|
void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) {
|
|
bot_input_t bi;
|
|
int j;
|
|
|
|
//add the delta angles to the bot's current view angles
|
|
for (j = 0; j < 3; j++) {
|
|
bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
|
|
}
|
|
//change the bot view angles
|
|
BotChangeViewAngles(bs, (float) elapsed_time / 1000);
|
|
//retrieve the bot input
|
|
trap_EA_GetInput(bs->client, (float) time / 1000, &bi);
|
|
//respawn hack
|
|
if (bi.actionflags & ACTION_RESPAWN) {
|
|
if (bs->lastucmd.buttons & BUTTON_ATTACK) bi.actionflags &= ~(ACTION_RESPAWN|ACTION_ATTACK);
|
|
}
|
|
//convert the bot input to a usercmd
|
|
BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time);
|
|
//subtract the delta angles
|
|
for (j = 0; j < 3; j++) {
|
|
bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAIRegularUpdate
|
|
==============
|
|
*/
|
|
void BotAIRegularUpdate(void) {
|
|
if (regularupdate_time < FloatTime()) {
|
|
trap_BotUpdateEntityItems();
|
|
regularupdate_time = FloatTime() + 0.3;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
RemoveColorEscapeSequences
|
|
==============
|
|
*/
|
|
void RemoveColorEscapeSequences( char *text ) {
|
|
int i, l;
|
|
|
|
l = 0;
|
|
for ( i = 0; text[i]; i++ ) {
|
|
if (Q_IsColorString(&text[i])) {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (text[i] > 0x7E)
|
|
continue;
|
|
text[l++] = text[i];
|
|
}
|
|
text[l] = '\0';
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAI
|
|
==============
|
|
*/
|
|
int BotAI(int client, float thinktime) {
|
|
bot_state_t *bs;
|
|
char buf[1024], *args;
|
|
int j;
|
|
|
|
trap_EA_ResetInput(client);
|
|
//
|
|
bs = botstates[client];
|
|
if (!bs || !bs->inuse) {
|
|
BotAI_Print(PRT_FATAL, "BotAI: client %d is not setup\n", client);
|
|
return qfalse;
|
|
}
|
|
|
|
//retrieve the current client state
|
|
BotAI_GetClientState( client, &bs->cur_ps );
|
|
|
|
//retrieve any waiting server commands
|
|
while( trap_BotGetServerCommand(client, buf, sizeof(buf)) ) {
|
|
//have buf point to the command and args to the command arguments
|
|
args = strchr( buf, ' ');
|
|
if (!args) continue;
|
|
*args++ = '\0';
|
|
|
|
//remove color espace sequences from the arguments
|
|
RemoveColorEscapeSequences( args );
|
|
|
|
if (!Q_stricmp(buf, "cp "))
|
|
{ /*CenterPrintf*/ }
|
|
else if (!Q_stricmp(buf, "cs"))
|
|
{ /*ConfigStringModified*/ }
|
|
else if (!Q_stricmp(buf, "print")) {
|
|
//remove first and last quote from the chat message
|
|
memmove(args, args+1, strlen(args));
|
|
args[strlen(args)-1] = '\0';
|
|
trap_BotQueueConsoleMessage(bs->cs, CMS_NORMAL, args);
|
|
}
|
|
else if (!Q_stricmp(buf, "chat")) {
|
|
//remove first and last quote from the chat message
|
|
memmove(args, args+1, strlen(args));
|
|
args[strlen(args)-1] = '\0';
|
|
trap_BotQueueConsoleMessage(bs->cs, CMS_CHAT, args);
|
|
}
|
|
else if (!Q_stricmp(buf, "tchat")) {
|
|
//remove first and last quote from the chat message
|
|
memmove(args, args+1, strlen(args));
|
|
args[strlen(args)-1] = '\0';
|
|
trap_BotQueueConsoleMessage(bs->cs, CMS_CHAT, args);
|
|
}
|
|
#ifdef MISSIONPACK
|
|
else if (!Q_stricmp(buf, "vchat")) {
|
|
BotVoiceChatCommand(bs, SAY_ALL, args);
|
|
}
|
|
else if (!Q_stricmp(buf, "vtchat")) {
|
|
BotVoiceChatCommand(bs, SAY_TEAM, args);
|
|
}
|
|
else if (!Q_stricmp(buf, "vtell")) {
|
|
BotVoiceChatCommand(bs, SAY_TELL, args);
|
|
}
|
|
#endif
|
|
else if (!Q_stricmp(buf, "scores"))
|
|
{ /*FIXME: parse scores?*/ }
|
|
else if (!Q_stricmp(buf, "clientLevelShot"))
|
|
{ /*ignore*/ }
|
|
}
|
|
//add the delta angles to the bot's current view angles
|
|
for (j = 0; j < 3; j++) {
|
|
bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
|
|
}
|
|
//increase the local time of the bot
|
|
bs->ltime += thinktime;
|
|
//
|
|
bs->thinktime = thinktime;
|
|
//origin of the bot
|
|
VectorCopy(bs->cur_ps.origin, bs->origin);
|
|
//eye coordinates of the bot
|
|
VectorCopy(bs->cur_ps.origin, bs->eye);
|
|
bs->eye[2] += bs->cur_ps.viewheight;
|
|
//get the area the bot is in
|
|
bs->areanum = BotPointAreaNum(bs->origin);
|
|
//the real AI
|
|
BotDeathmatchAI(bs, thinktime);
|
|
//set the weapon selection every AI frame
|
|
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
|
|
//subtract the delta angles
|
|
for (j = 0; j < 3; j++) {
|
|
bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
|
|
}
|
|
//everything was ok
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
BotScheduleBotThink
|
|
==================
|
|
*/
|
|
void BotScheduleBotThink(void) {
|
|
int i, botnum;
|
|
|
|
botnum = 0;
|
|
|
|
for( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if( !botstates[i] || !botstates[i]->inuse ) {
|
|
continue;
|
|
}
|
|
//initialize the bot think residual time
|
|
botstates[i]->botthink_residual = bot_thinktime.integer * botnum / numbots;
|
|
botnum++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotWriteSessionData
|
|
==============
|
|
*/
|
|
void BotWriteSessionData(bot_state_t *bs) {
|
|
const char *s;
|
|
const char *var;
|
|
|
|
s = va(
|
|
"%i %i %i %i %i %i %i %i"
|
|
" %f %f %f"
|
|
" %f %f %f"
|
|
" %f %f %f"
|
|
" %f",
|
|
bs->lastgoal_decisionmaker,
|
|
bs->lastgoal_ltgtype,
|
|
bs->lastgoal_teammate,
|
|
bs->lastgoal_teamgoal.areanum,
|
|
bs->lastgoal_teamgoal.entitynum,
|
|
bs->lastgoal_teamgoal.flags,
|
|
bs->lastgoal_teamgoal.iteminfo,
|
|
bs->lastgoal_teamgoal.number,
|
|
bs->lastgoal_teamgoal.origin[0],
|
|
bs->lastgoal_teamgoal.origin[1],
|
|
bs->lastgoal_teamgoal.origin[2],
|
|
bs->lastgoal_teamgoal.mins[0],
|
|
bs->lastgoal_teamgoal.mins[1],
|
|
bs->lastgoal_teamgoal.mins[2],
|
|
bs->lastgoal_teamgoal.maxs[0],
|
|
bs->lastgoal_teamgoal.maxs[1],
|
|
bs->lastgoal_teamgoal.maxs[2],
|
|
bs->formation_dist
|
|
);
|
|
|
|
var = va( "botsession%i", bs->client );
|
|
|
|
trap_Cvar_Set( var, s );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotReadSessionData
|
|
==============
|
|
*/
|
|
void BotReadSessionData(bot_state_t *bs) {
|
|
char s[MAX_STRING_CHARS];
|
|
const char *var;
|
|
|
|
var = va( "botsession%i", bs->client );
|
|
trap_Cvar_VariableStringBuffer( var, s, sizeof(s) );
|
|
|
|
sscanf(s,
|
|
"%i %i %i %i %i %i %i %i"
|
|
" %f %f %f"
|
|
" %f %f %f"
|
|
" %f %f %f"
|
|
" %f",
|
|
&bs->lastgoal_decisionmaker,
|
|
&bs->lastgoal_ltgtype,
|
|
&bs->lastgoal_teammate,
|
|
&bs->lastgoal_teamgoal.areanum,
|
|
&bs->lastgoal_teamgoal.entitynum,
|
|
&bs->lastgoal_teamgoal.flags,
|
|
&bs->lastgoal_teamgoal.iteminfo,
|
|
&bs->lastgoal_teamgoal.number,
|
|
&bs->lastgoal_teamgoal.origin[0],
|
|
&bs->lastgoal_teamgoal.origin[1],
|
|
&bs->lastgoal_teamgoal.origin[2],
|
|
&bs->lastgoal_teamgoal.mins[0],
|
|
&bs->lastgoal_teamgoal.mins[1],
|
|
&bs->lastgoal_teamgoal.mins[2],
|
|
&bs->lastgoal_teamgoal.maxs[0],
|
|
&bs->lastgoal_teamgoal.maxs[1],
|
|
&bs->lastgoal_teamgoal.maxs[2],
|
|
&bs->formation_dist
|
|
);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAISetupClient
|
|
==============
|
|
*/
|
|
int BotAISetupClient(int client, struct bot_settings_s *settings, qboolean restart) {
|
|
char filename[MAX_PATH], name[MAX_PATH], gender[MAX_PATH];
|
|
bot_state_t *bs;
|
|
int errnum;
|
|
|
|
if (!botstates[client]) botstates[client] = G_Alloc(sizeof(bot_state_t));
|
|
bs = botstates[client];
|
|
|
|
if (!bs) {
|
|
return qfalse;
|
|
}
|
|
|
|
if (bs && bs->inuse) {
|
|
BotAI_Print(PRT_FATAL, "BotAISetupClient: client %d already setup\n", client);
|
|
return qfalse;
|
|
}
|
|
|
|
if (!trap_AAS_Initialized()) {
|
|
BotAI_Print(PRT_FATAL, "AAS not initialized\n");
|
|
return qfalse;
|
|
}
|
|
|
|
//load the bot character
|
|
bs->character = trap_BotLoadCharacter(settings->characterfile, settings->skill);
|
|
if (!bs->character) {
|
|
BotAI_Print(PRT_FATAL, "couldn't load skill %f from %s\n", settings->skill, settings->characterfile);
|
|
return qfalse;
|
|
}
|
|
//copy the settings
|
|
memcpy(&bs->settings, settings, sizeof(bot_settings_t));
|
|
//allocate a goal state
|
|
bs->gs = trap_BotAllocGoalState(client);
|
|
//load the item weights
|
|
trap_Characteristic_String(bs->character, CHARACTERISTIC_ITEMWEIGHTS, filename, MAX_PATH);
|
|
errnum = trap_BotLoadItemWeights(bs->gs, filename);
|
|
if (errnum != BLERR_NOERROR) {
|
|
trap_BotFreeGoalState(bs->gs);
|
|
return qfalse;
|
|
}
|
|
//allocate a weapon state
|
|
bs->ws = trap_BotAllocWeaponState();
|
|
//load the weapon weights
|
|
trap_Characteristic_String(bs->character, CHARACTERISTIC_WEAPONWEIGHTS, filename, MAX_PATH);
|
|
errnum = trap_BotLoadWeaponWeights(bs->ws, filename);
|
|
if (errnum != BLERR_NOERROR) {
|
|
trap_BotFreeGoalState(bs->gs);
|
|
trap_BotFreeWeaponState(bs->ws);
|
|
return qfalse;
|
|
}
|
|
//allocate a chat state
|
|
bs->cs = trap_BotAllocChatState();
|
|
//load the chat file
|
|
trap_Characteristic_String(bs->character, CHARACTERISTIC_CHAT_FILE, filename, MAX_PATH);
|
|
trap_Characteristic_String(bs->character, CHARACTERISTIC_CHAT_NAME, name, MAX_PATH);
|
|
errnum = trap_BotLoadChatFile(bs->cs, filename, name);
|
|
if (errnum != BLERR_NOERROR) {
|
|
trap_BotFreeChatState(bs->cs);
|
|
trap_BotFreeGoalState(bs->gs);
|
|
trap_BotFreeWeaponState(bs->ws);
|
|
return qfalse;
|
|
}
|
|
//get the gender characteristic
|
|
trap_Characteristic_String(bs->character, CHARACTERISTIC_GENDER, gender, MAX_PATH);
|
|
//set the chat gender
|
|
if (*gender == 'f' || *gender == 'F') trap_BotSetChatGender(bs->cs, CHAT_GENDERFEMALE);
|
|
else if (*gender == 'm' || *gender == 'M') trap_BotSetChatGender(bs->cs, CHAT_GENDERMALE);
|
|
else trap_BotSetChatGender(bs->cs, CHAT_GENDERLESS);
|
|
|
|
bs->inuse = qtrue;
|
|
bs->client = client;
|
|
bs->entitynum = client;
|
|
bs->setupcount = 4;
|
|
bs->entergame_time = FloatTime();
|
|
bs->ms = trap_BotAllocMoveState();
|
|
bs->walker = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_WALKER, 0, 1);
|
|
numbots++;
|
|
|
|
if (trap_Cvar_VariableIntegerValue("bot_testichat")) {
|
|
trap_BotLibVarSet("bot_testichat", "1");
|
|
BotChatTest(bs);
|
|
}
|
|
//NOTE: reschedule the bot thinking
|
|
BotScheduleBotThink();
|
|
//if interbreeding start with a mutation
|
|
if (bot_interbreed) {
|
|
trap_BotMutateGoalFuzzyLogic(bs->gs, 1);
|
|
}
|
|
// if we kept the bot client
|
|
if (restart) {
|
|
BotReadSessionData(bs);
|
|
}
|
|
//bot has been setup succesfully
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAIShutdownClient
|
|
==============
|
|
*/
|
|
int BotAIShutdownClient(int client, qboolean restart) {
|
|
bot_state_t *bs;
|
|
|
|
bs = botstates[client];
|
|
if (!bs || !bs->inuse) {
|
|
//BotAI_Print(PRT_ERROR, "BotAIShutdownClient: client %d already shutdown\n", client);
|
|
return qfalse;
|
|
}
|
|
|
|
if (restart) {
|
|
BotWriteSessionData(bs);
|
|
}
|
|
|
|
if (BotChat_ExitGame(bs)) {
|
|
trap_BotEnterChat(bs->cs, bs->client, CHAT_ALL);
|
|
}
|
|
|
|
trap_BotFreeMoveState(bs->ms);
|
|
//free the goal state
|
|
trap_BotFreeGoalState(bs->gs);
|
|
//free the chat file
|
|
trap_BotFreeChatState(bs->cs);
|
|
//free the weapon weights
|
|
trap_BotFreeWeaponState(bs->ws);
|
|
//free the bot character
|
|
trap_BotFreeCharacter(bs->character);
|
|
//
|
|
BotFreeWaypoints(bs->checkpoints);
|
|
BotFreeWaypoints(bs->patrolpoints);
|
|
//clear activate goal stack
|
|
BotClearActivateGoalStack(bs);
|
|
//clear the bot state
|
|
memset(bs, 0, sizeof(bot_state_t));
|
|
//set the inuse flag to qfalse
|
|
bs->inuse = qfalse;
|
|
//there's one bot less
|
|
numbots--;
|
|
//everything went ok
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotResetState
|
|
|
|
called when a bot enters the intermission or observer mode and
|
|
when the level is changed
|
|
==============
|
|
*/
|
|
void BotResetState(bot_state_t *bs) {
|
|
int client, entitynum, inuse;
|
|
int movestate, goalstate, chatstate, weaponstate;
|
|
bot_settings_t settings;
|
|
int character;
|
|
playerState_t ps; //current player state
|
|
float entergame_time;
|
|
|
|
//save some things that should not be reset here
|
|
memcpy(&settings, &bs->settings, sizeof(bot_settings_t));
|
|
memcpy(&ps, &bs->cur_ps, sizeof(playerState_t));
|
|
inuse = bs->inuse;
|
|
client = bs->client;
|
|
entitynum = bs->entitynum;
|
|
character = bs->character;
|
|
movestate = bs->ms;
|
|
goalstate = bs->gs;
|
|
chatstate = bs->cs;
|
|
weaponstate = bs->ws;
|
|
entergame_time = bs->entergame_time;
|
|
//free checkpoints and patrol points
|
|
BotFreeWaypoints(bs->checkpoints);
|
|
BotFreeWaypoints(bs->patrolpoints);
|
|
//reset the whole state
|
|
memset(bs, 0, sizeof(bot_state_t));
|
|
//copy back some state stuff that should not be reset
|
|
bs->ms = movestate;
|
|
bs->gs = goalstate;
|
|
bs->cs = chatstate;
|
|
bs->ws = weaponstate;
|
|
memcpy(&bs->cur_ps, &ps, sizeof(playerState_t));
|
|
memcpy(&bs->settings, &settings, sizeof(bot_settings_t));
|
|
bs->inuse = inuse;
|
|
bs->client = client;
|
|
bs->entitynum = entitynum;
|
|
bs->character = character;
|
|
bs->entergame_time = entergame_time;
|
|
//reset several states
|
|
if (bs->ms) trap_BotResetMoveState(bs->ms);
|
|
if (bs->gs) trap_BotResetGoalState(bs->gs);
|
|
if (bs->ws) trap_BotResetWeaponState(bs->ws);
|
|
if (bs->gs) trap_BotResetAvoidGoals(bs->gs);
|
|
if (bs->ms) trap_BotResetAvoidReach(bs->ms);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAILoadMap
|
|
==============
|
|
*/
|
|
int BotAILoadMap( int restart ) {
|
|
int i;
|
|
vmCvar_t mapname;
|
|
|
|
if (!restart) {
|
|
trap_Cvar_Register( &mapname, "mapname", "", CVAR_SERVERINFO | CVAR_ROM );
|
|
trap_BotLibLoadMap( mapname.string );
|
|
}
|
|
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if (botstates[i] && botstates[i]->inuse) {
|
|
BotResetState( botstates[i] );
|
|
botstates[i]->setupcount = 4;
|
|
}
|
|
}
|
|
|
|
BotSetupDeathmatchAI();
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
void ProximityMine_Trigger( gentity_t *trigger, gentity_t *other, trace_t *trace );
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
BotAIStartFrame
|
|
==================
|
|
*/
|
|
int BotAIStartFrame(int time) {
|
|
int i;
|
|
gentity_t *ent;
|
|
bot_entitystate_t state;
|
|
int elapsed_time, thinktime;
|
|
static int local_time;
|
|
static int botlib_residual;
|
|
static int lastbotthink_time;
|
|
|
|
G_CheckBotSpawn();
|
|
|
|
trap_Cvar_Update(&bot_rocketjump);
|
|
trap_Cvar_Update(&bot_grapple);
|
|
trap_Cvar_Update(&bot_fastchat);
|
|
trap_Cvar_Update(&bot_nochat);
|
|
trap_Cvar_Update(&bot_testrchat);
|
|
trap_Cvar_Update(&bot_thinktime);
|
|
trap_Cvar_Update(&bot_memorydump);
|
|
trap_Cvar_Update(&bot_saveroutingcache);
|
|
trap_Cvar_Update(&bot_pause);
|
|
trap_Cvar_Update(&bot_report);
|
|
|
|
if (bot_report.integer) {
|
|
// BotTeamplayReport();
|
|
// trap_Cvar_Set("bot_report", "0");
|
|
BotUpdateInfoConfigStrings();
|
|
}
|
|
|
|
if (bot_pause.integer) {
|
|
// execute bot user commands every frame
|
|
for( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if( !botstates[i] || !botstates[i]->inuse ) {
|
|
continue;
|
|
}
|
|
if( g_entities[i].client->pers.connected != CON_CONNECTED ) {
|
|
continue;
|
|
}
|
|
botstates[i]->lastucmd.forwardmove = 0;
|
|
botstates[i]->lastucmd.rightmove = 0;
|
|
botstates[i]->lastucmd.upmove = 0;
|
|
botstates[i]->lastucmd.buttons = 0;
|
|
botstates[i]->lastucmd.serverTime = time;
|
|
trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd);
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
if (bot_memorydump.integer) {
|
|
trap_BotLibVarSet("memorydump", "1");
|
|
trap_Cvar_Set("bot_memorydump", "0");
|
|
}
|
|
if (bot_saveroutingcache.integer) {
|
|
trap_BotLibVarSet("saveroutingcache", "1");
|
|
trap_Cvar_Set("bot_saveroutingcache", "0");
|
|
}
|
|
//check if bot interbreeding is activated
|
|
BotInterbreeding();
|
|
//cap the bot think time
|
|
if (bot_thinktime.integer > 200) {
|
|
trap_Cvar_Set("bot_thinktime", "200");
|
|
}
|
|
//if the bot think time changed we should reschedule the bots
|
|
if (bot_thinktime.integer != lastbotthink_time) {
|
|
lastbotthink_time = bot_thinktime.integer;
|
|
BotScheduleBotThink();
|
|
}
|
|
|
|
elapsed_time = time - local_time;
|
|
local_time = time;
|
|
|
|
botlib_residual += elapsed_time;
|
|
|
|
if (elapsed_time > bot_thinktime.integer) thinktime = elapsed_time;
|
|
else thinktime = bot_thinktime.integer;
|
|
|
|
// update the bot library
|
|
if ( botlib_residual >= thinktime ) {
|
|
botlib_residual -= thinktime;
|
|
|
|
trap_BotLibStartFrame((float) time / 1000);
|
|
|
|
if (!trap_AAS_Initialized()) return qfalse;
|
|
|
|
//update entities in the botlib
|
|
for (i = 0; i < MAX_GENTITIES; i++) {
|
|
ent = &g_entities[i];
|
|
if (!ent->inuse) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
if (!ent->r.linked) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
if (ent->r.svFlags & SVF_NOCLIENT) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
// do not update missiles
|
|
if (ent->s.eType == ET_MISSILE && ent->s.weapon != WP_GRAPPLING_HOOK) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
// do not update event only entities
|
|
if (ent->s.eType > ET_EVENTS) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
#ifdef MISSIONPACK
|
|
// never link prox mine triggers
|
|
if (ent->r.contents == CONTENTS_TRIGGER) {
|
|
if (ent->touch == ProximityMine_Trigger) {
|
|
trap_BotLibUpdateEntity(i, NULL);
|
|
continue;
|
|
}
|
|
}
|
|
#endif
|
|
//
|
|
memset(&state, 0, sizeof(bot_entitystate_t));
|
|
//
|
|
VectorCopy(ent->r.currentOrigin, state.origin);
|
|
if (i < MAX_CLIENTS) {
|
|
VectorCopy(ent->s.apos.trBase, state.angles);
|
|
} else {
|
|
VectorCopy(ent->r.currentAngles, state.angles);
|
|
}
|
|
VectorCopy(ent->s.origin2, state.old_origin);
|
|
VectorCopy(ent->r.mins, state.mins);
|
|
VectorCopy(ent->r.maxs, state.maxs);
|
|
state.type = ent->s.eType;
|
|
state.flags = ent->s.eFlags;
|
|
if (ent->r.bmodel) state.solid = SOLID_BSP;
|
|
else state.solid = SOLID_BBOX;
|
|
state.groundent = ent->s.groundEntityNum;
|
|
state.modelindex = ent->s.modelindex;
|
|
state.modelindex2 = ent->s.modelindex2;
|
|
state.frame = ent->s.frame;
|
|
state.event = ent->s.event;
|
|
state.eventParm = ent->s.eventParm;
|
|
state.powerups = ent->s.powerups;
|
|
state.legsAnim = ent->s.legsAnim;
|
|
state.torsoAnim = ent->s.torsoAnim;
|
|
state.weapon = ent->s.weapon;
|
|
//
|
|
trap_BotLibUpdateEntity(i, &state);
|
|
}
|
|
|
|
BotAIRegularUpdate();
|
|
}
|
|
|
|
floattime = trap_AAS_Time();
|
|
|
|
// execute scheduled bot AI
|
|
for( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if( !botstates[i] || !botstates[i]->inuse ) {
|
|
continue;
|
|
}
|
|
//
|
|
botstates[i]->botthink_residual += elapsed_time;
|
|
//
|
|
if ( botstates[i]->botthink_residual >= thinktime ) {
|
|
botstates[i]->botthink_residual -= thinktime;
|
|
|
|
if (!trap_AAS_Initialized()) return qfalse;
|
|
|
|
if (g_entities[i].client->pers.connected == CON_CONNECTED) {
|
|
BotAI(i, (float) thinktime / 1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// execute bot user commands every frame
|
|
for( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if( !botstates[i] || !botstates[i]->inuse ) {
|
|
continue;
|
|
}
|
|
if( g_entities[i].client->pers.connected != CON_CONNECTED ) {
|
|
continue;
|
|
}
|
|
|
|
BotUpdateInput(botstates[i], time, elapsed_time);
|
|
trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd);
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotInitLibrary
|
|
==============
|
|
*/
|
|
int BotInitLibrary(void) {
|
|
char buf[144];
|
|
|
|
//set the maxclients and maxentities library variables before calling BotSetupLibrary
|
|
trap_Cvar_VariableStringBuffer("sv_maxclients", buf, sizeof(buf));
|
|
if (!strlen(buf)) strcpy(buf, "8");
|
|
trap_BotLibVarSet("maxclients", buf);
|
|
Com_sprintf(buf, sizeof(buf), "%d", MAX_GENTITIES);
|
|
trap_BotLibVarSet("maxentities", buf);
|
|
//bsp checksum
|
|
trap_Cvar_VariableStringBuffer("sv_mapChecksum", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("sv_mapChecksum", buf);
|
|
//maximum number of aas links
|
|
trap_Cvar_VariableStringBuffer("max_aaslinks", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("max_aaslinks", buf);
|
|
//maximum number of items in a level
|
|
trap_Cvar_VariableStringBuffer("max_levelitems", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("max_levelitems", buf);
|
|
//game type
|
|
trap_Cvar_VariableStringBuffer("g_gametype", buf, sizeof(buf));
|
|
if (!strlen(buf)) strcpy(buf, "0");
|
|
trap_BotLibVarSet("g_gametype", buf);
|
|
//bot developer mode and log file
|
|
trap_BotLibVarSet("bot_developer", bot_developer.string);
|
|
trap_Cvar_VariableStringBuffer("logfile", buf, sizeof(buf));
|
|
trap_BotLibVarSet("log", buf);
|
|
//no chatting
|
|
trap_Cvar_VariableStringBuffer("bot_nochat", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("nochat", buf);
|
|
//visualize jump pads
|
|
trap_Cvar_VariableStringBuffer("bot_visualizejumppads", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("bot_visualizejumppads", buf);
|
|
//forced clustering calculations
|
|
trap_Cvar_VariableStringBuffer("bot_forceclustering", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("forceclustering", buf);
|
|
//forced reachability calculations
|
|
trap_Cvar_VariableStringBuffer("bot_forcereachability", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("forcereachability", buf);
|
|
//force writing of AAS to file
|
|
trap_Cvar_VariableStringBuffer("bot_forcewrite", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("forcewrite", buf);
|
|
//no AAS optimization
|
|
trap_Cvar_VariableStringBuffer("bot_aasoptimize", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("aasoptimize", buf);
|
|
//
|
|
trap_Cvar_VariableStringBuffer("bot_saveroutingcache", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("saveroutingcache", buf);
|
|
//reload instead of cache bot character files
|
|
trap_Cvar_VariableStringBuffer("bot_reloadcharacters", buf, sizeof(buf));
|
|
if (!strlen(buf)) strcpy(buf, "0");
|
|
trap_BotLibVarSet("bot_reloadcharacters", buf);
|
|
//base directory
|
|
trap_Cvar_VariableStringBuffer("fs_basepath", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("basedir", buf);
|
|
//game directory
|
|
trap_Cvar_VariableStringBuffer("fs_game", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("gamedir", buf);
|
|
//home directory
|
|
trap_Cvar_VariableStringBuffer("fs_homepath", buf, sizeof(buf));
|
|
if (strlen(buf)) trap_BotLibVarSet("homedir", buf);
|
|
//
|
|
#ifdef MISSIONPACK
|
|
trap_BotLibDefine("MISSIONPACK");
|
|
#endif
|
|
//setup the bot library
|
|
return trap_BotLibSetup();
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAISetup
|
|
==============
|
|
*/
|
|
int BotAISetup( int restart ) {
|
|
int errnum;
|
|
|
|
trap_Cvar_Register(&bot_thinktime, "bot_thinktime", "100", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_memorydump, "bot_memorydump", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_saveroutingcache, "bot_saveroutingcache", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_pause, "bot_pause", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_report, "bot_report", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_testsolid, "bot_testsolid", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_testclusters, "bot_testclusters", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_developer, "bot_developer", "0", CVAR_CHEAT);
|
|
trap_Cvar_Register(&bot_interbreedchar, "bot_interbreedchar", "", 0);
|
|
trap_Cvar_Register(&bot_interbreedbots, "bot_interbreedbots", "10", 0);
|
|
trap_Cvar_Register(&bot_interbreedcycle, "bot_interbreedcycle", "20", 0);
|
|
trap_Cvar_Register(&bot_interbreedwrite, "bot_interbreedwrite", "", 0);
|
|
|
|
//if the game is restarted for a tournament
|
|
if (restart) {
|
|
return qtrue;
|
|
}
|
|
|
|
//initialize the bot states
|
|
memset( botstates, 0, sizeof(botstates) );
|
|
|
|
errnum = BotInitLibrary();
|
|
if (errnum != BLERR_NOERROR) return qfalse;
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
BotAIShutdown
|
|
==============
|
|
*/
|
|
int BotAIShutdown( int restart ) {
|
|
|
|
int i;
|
|
|
|
//if the game is restarted for a tournament
|
|
if ( restart ) {
|
|
//shutdown all the bots in the botlib
|
|
for (i = 0; i < MAX_CLIENTS; i++) {
|
|
if (botstates[i] && botstates[i]->inuse) {
|
|
BotAIShutdownClient(botstates[i]->client, restart);
|
|
}
|
|
}
|
|
//don't shutdown the bot library
|
|
}
|
|
else {
|
|
trap_BotLibShutdown();
|
|
}
|
|
return qtrue;
|
|
}
|
|
|