/* =========================================================================== Wolfenstein: Enemy Territory GPL Source Code Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company. This file is part of the Wolfenstein: Enemy Territory GPL Source Code (“Wolf ET Source Code”). Wolf ET 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 3 of the License, or (at your option) any later version. Wolf ET 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 Wolf ET Source Code. If not, see . In addition, the Wolf: ET Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Wolf ET Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ // g_bot.c #include "../game/g_local.h" #include "../game/q_shared.h" #include "../game/botlib.h" //bot lib interface #include "../game/be_aas.h" #include "../game/be_ea.h" #include "../game/be_ai_gen.h" #include "../game/be_ai_goal.h" #include "../game/be_ai_move.h" #include "../botai/botai.h" //bot ai interface #include "../botai/ai_main.h" #include "../botai/chars.h" #include "../botai/ai_team.h" #include "../botai/ai_dmq3.h" #include "../game/be_ai_chat.h" static int g_numBots; static char g_botInfos[MAX_BOTS][MAX_INFO_STRING]; int g_numArenas; static char g_arenaInfos[MAX_ARENAS][MAX_INFO_STRING]; #define BOT_BEGIN_DELAY_BASE 2000 #define BOT_BEGIN_DELAY_INCREMENT 1500 #define BOT_SPAWN_QUEUE_DEPTH 16 typedef struct { int clientNum; int spawnTime; } botSpawnQueue_t; static int botBeginDelay; static botSpawnQueue_t botSpawnQueue[BOT_SPAWN_QUEUE_DEPTH]; vmCvar_t bot_minplayers; vmCvar_t bot_eventeams; vmCvar_t bot_defaultskill; // Mad Doc - TDF // for bot debugging purposes only. vmCvar_t bot_debug; // if set, draw "thought bubbles" for crosshair-selected bot vmCvar_t bot_debug_curAINode; // the text of the current ainode for the bot begin debugged vmCvar_t bot_debug_alertState; // alert state of the bot being debugged vmCvar_t bot_debug_pos; // coords of the bot being debugged vmCvar_t bot_debug_weaponAutonomy; // weapon autonomy of the bot being debugged vmCvar_t bot_debug_movementAutonomy; // movement autonomy of the bot being debugged vmCvar_t bot_debug_cover_spot; // What cover spot are we going to? vmCvar_t bot_debug_anim; // what animation is the bot playing? extern gentity_t *podium1; extern gentity_t *podium2; extern gentity_t *podium3; // TTimo gcc: defined but not used #if 0 /* =============== G_LoadArenas =============== */ static void G_LoadArenas( void ) { #ifdef QUAKESTUFF int len; char *filename; vmCvar_t arenasFile; fileHandle_t f; int n; char buf[MAX_ARENAS_TEXT]; trap_Cvar_Register( &arenasFile, "g_arenasFile", "", CVAR_INIT | CVAR_ROM ); if ( *arenasFile.string ) { filename = arenasFile.string; } else { filename = "scripts/arenas.txt"; } len = trap_FS_FOpenFile( filename, &f, FS_READ ); if ( !f ) { trap_Printf( va( S_COLOR_RED "file not found: %s\n", filename ) ); return; } if ( len >= MAX_ARENAS_TEXT ) { trap_Printf( va( S_COLOR_RED "file too large: %s is %i, max allowed is %i", filename, len, MAX_ARENAS_TEXT ) ); trap_FS_FCloseFile( f ); return; } trap_FS_Read( buf, len, f ); buf[len] = 0; trap_FS_FCloseFile( f ); g_numArenas = Com_ParseInfos( buf, MAX_ARENAS, g_arenaInfos ); trap_Printf( va( "%i arenas parsed\n", g_numArenas ) ); for ( n = 0; n < g_numArenas; n++ ) { Info_SetValueForKey( g_arenaInfos[n], "num", va( "%i", n ) ); } #endif } #endif /* =============== G_GetArenaInfoByNumber =============== */ const char *G_GetArenaInfoByMap( const char *map ) { int n; for ( n = 0; n < g_numArenas; n++ ) { if ( Q_stricmp( Info_ValueForKey( g_arenaInfos[n], "map" ), map ) == 0 ) { return g_arenaInfos[n]; } } return NULL; } // TTimo: defined but not used #if 0 /* ================= PlayerIntroSound ================= */ static void PlayerIntroSound( const char *modelAndSkin ) { char model[MAX_QPATH]; char *skin; Q_strncpyz( model, modelAndSkin, sizeof( model ) ); skin = Q_strrchr( model, '/' ); if ( skin ) { *skin++ = '\0'; } else { skin = model; } if ( Q_stricmp( skin, "default" ) == 0 ) { skin = model; } trap_SendConsoleCommand( EXEC_APPEND, va( "play sound/player/announce/%s.wav\n", skin ) ); } #endif /* =============== G_AddRandomBot =============== */ void G_AddRandomBot( int team ) { char *teamstr; int skill; skill = trap_Cvar_VariableIntegerValue( "bot_defaultskill" ); if ( team == TEAM_AXIS ) { teamstr = "red"; } else if ( team == TEAM_ALLIES ) { teamstr = "blue"; } else { teamstr = "";} trap_SendConsoleCommand( EXEC_INSERT, va( "addbot %i %s %i\n", skill, teamstr, 0 ) ); /* int i, n, num, skill; char *value, netname[36], *teamstr; gclient_t *cl; num = 0; for ( n = 0; n < g_numBots ; n++ ) { value = Info_ValueForKey( g_botInfos[n], "name" ); // for ( i=0 ; i< g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( !(g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT) ) { continue; } if ( team >= 0 && cl->sess.sessionTeam != team ) { continue; } if ( !Q_stricmp( value, cl->pers.netname ) ) { break; } } if (i >= g_maxclients.integer) { num++; } } num = random() * num; for ( n = 0; n < g_numBots ; n++ ) { value = Info_ValueForKey( g_botInfos[n], "name" ); // for ( i=0 ; i< g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( !(g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT) ) { continue; } if ( team >= 0 && cl->sess.sessionTeam != team ) { continue; } if ( !Q_stricmp( value, cl->pers.netname ) ) { break; } } if (i >= g_maxclients.integer) { num--; if (num <= 0) { skill = trap_Cvar_VariableIntegerValue( "g_spSkill" ); if (team == TEAM_AXIS) teamstr = "red"; else if (team == TEAM_ALLIES) teamstr = "blue"; else teamstr = ""; strncpy(netname, value, sizeof(netname)-1); netname[sizeof(netname)-1] = '\0'; Q_CleanStr(netname); trap_SendConsoleCommand( EXEC_INSERT, va("addbot %i %s %i\n", skill, teamstr, 0) ); return; } } } */ } /* =============== G_RemoveRandomBot =============== */ int G_RemoveRandomBot( int team ) { int i; char netname[36]; gclient_t *cl; for ( i = 0 ; i < g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( !( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) ) { continue; } if ( team >= 0 && cl->sess.sessionTeam != team ) { continue; } strcpy( netname, cl->pers.netname ); Q_CleanStr( netname ); trap_SendConsoleCommand( EXEC_INSERT, va( "kick \"%s\" 0\n", netname ) ); return qtrue; } return qfalse; } /* =============== G_CountHumanPlayers =============== */ int G_CountHumanPlayers( int team ) { int i, num; gclient_t *cl; num = 0; for ( i = 0 ; i < g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) { continue; } if ( team >= 0 && cl->sess.sessionTeam != team ) { continue; } num++; } return num; } /* =============== G_CountBotPlayers =============== */ int G_CountBotPlayers( int team ) { int i, n, num; gclient_t *cl; num = 0; for ( i = 0 ; i < g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( !( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) ) { continue; } if ( team >= 0 && cl->sess.sessionTeam != team ) { continue; } num++; } for ( n = 0; n < BOT_SPAWN_QUEUE_DEPTH; n++ ) { if ( !botSpawnQueue[n].spawnTime ) { continue; } if ( botSpawnQueue[n].spawnTime > level.time ) { continue; } num++; } return num; } /* =============== G_CheckMinimumPlayers =============== */ void G_CheckMinimumPlayers( void ) { int minplayers /*, weakestTeam, strongestTeam*/; int humanplayers[TEAM_NUM_TEAMS], botplayers[TEAM_NUM_TEAMS], players[TEAM_NUM_TEAMS]; static int checkminimumplayers_time = 0; // wait until the system is ready if ( !level.initStaticEnts ) { return; } //only check once each second if ( checkminimumplayers_time < level.time && checkminimumplayers_time > level.time - 1000 ) { return; } // More safety on initial load for MP if ( !G_IsSinglePlayerGame() && level.time - level.startTime < 7500 ) { return; } humanplayers[TEAM_AXIS] = G_CountHumanPlayers( TEAM_AXIS ); botplayers[TEAM_AXIS] = G_CountBotPlayers( TEAM_AXIS ); players[TEAM_AXIS] = humanplayers[TEAM_AXIS] + botplayers[TEAM_AXIS]; // humanplayers[TEAM_ALLIES] = G_CountHumanPlayers( TEAM_ALLIES ); botplayers[TEAM_ALLIES] = G_CountBotPlayers( TEAM_ALLIES ); players[TEAM_ALLIES] = humanplayers[TEAM_ALLIES] + botplayers[TEAM_ALLIES]; checkminimumplayers_time = level.time; trap_Cvar_Update( &bot_minplayers ); minplayers = bot_minplayers.integer; if ( minplayers >= g_maxclients.integer / 2 ) { minplayers = ( g_maxclients.integer / 2 ) - 1; } // /* if (players[TEAM_AXIS] < minplayers) { G_AddRandomBot( TEAM_AXIS ); return; } // if (players[TEAM_ALLIES] < minplayers) { G_AddRandomBot( TEAM_ALLIES ); return; } // trap_Cvar_Update(&bot_eventeams); if (bot_eventeams.integer) { if (players[TEAM_AXIS] < players[TEAM_ALLIES]) { weakestTeam = TEAM_AXIS; strongestTeam = TEAM_ALLIES; } else if (players[TEAM_AXIS] > players[TEAM_ALLIES]) { weakestTeam = TEAM_ALLIES; strongestTeam = TEAM_AXIS; } else { return; // no changes required } // // even up the teams // if (!botplayers[strongestTeam] || (minplayers && players[weakestTeam] <= minplayers)) { // we have to add players to the weakestTeam G_AddRandomBot( weakestTeam ); return; } else { // remove players from the strongestTeam G_RemoveRandomBot( strongestTeam ); return; } } else if (minplayers) { // remove bots from teams with too many players if (players[TEAM_AXIS] > minplayers && botplayers[TEAM_AXIS]) { G_RemoveRandomBot( TEAM_AXIS ); return; } if (players[TEAM_ALLIES] > minplayers && botplayers[TEAM_ALLIES]) { G_RemoveRandomBot( TEAM_ALLIES ); return; } }*/ } /* =============== G_CheckBotSpawn =============== */ void G_CheckBotSpawn( void ) { int n; // char userinfo[MAX_INFO_VALUE]; G_CheckMinimumPlayers(); for ( n = 0; n < BOT_SPAWN_QUEUE_DEPTH; n++ ) { if ( !botSpawnQueue[n].spawnTime ) { continue; } if ( botSpawnQueue[n].spawnTime > level.time ) { continue; } ClientBegin( botSpawnQueue[n].clientNum ); botSpawnQueue[n].spawnTime = 0; } } /* =============== AddBotToSpawnQueue =============== */ static void AddBotToSpawnQueue( int clientNum, int delay ) { int n; for ( n = 0; n < BOT_SPAWN_QUEUE_DEPTH; n++ ) { if ( !botSpawnQueue[n].spawnTime ) { botSpawnQueue[n].spawnTime = level.time + delay; botSpawnQueue[n].clientNum = clientNum; return; } } G_Printf( S_COLOR_YELLOW "Unable to delay spawn\n" ); ClientBegin( clientNum ); } /* =============== G_QueueBotBegin =============== */ void G_QueueBotBegin( int clientNum ) { AddBotToSpawnQueue( clientNum, botBeginDelay ); botBeginDelay += BOT_BEGIN_DELAY_INCREMENT; } /* =============== G_BotConnect =============== */ qboolean G_BotConnect( int clientNum, qboolean restart ) { bot_settings_t settings; char userinfo[MAX_INFO_STRING]; trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); Q_strncpyz( settings.characterfile, Info_ValueForKey( userinfo, "characterfile" ), sizeof( settings.characterfile ) ); settings.skill = atoi( Info_ValueForKey( userinfo, "skill" ) ); Q_strncpyz( settings.team, Info_ValueForKey( userinfo, "team" ), sizeof( settings.team ) ); if ( !BotAISetupClient( clientNum, &settings ) ) { trap_DropClient( clientNum, "BotAISetupClient failed", 0 ); return qfalse; } return qtrue; } /* =============== Bot_GetWeaponForClassAndTeam =============== */ // returns the wp_ number for weapon "weaponName", or -1 if weapon not allowed for player/class/team combination int Bot_GetWeaponForClassAndTeam( int classNum, int teamNum, const char *weaponName ) { weapon_t weapon = -1; // start by assuming the weapon is invalid if ( !Q_stricmp( weaponName, "MP40" ) ) { weapon = WP_MP40; } else if ( !Q_stricmp( weaponName, "THOMPSON" ) ) { weapon = WP_THOMPSON; } else if ( !Q_stricmp( weaponName, "KAR98" ) ) { weapon = WP_KAR98; } else if ( !Q_stricmp( weaponName, "CARBINE" ) ) { weapon = WP_CARBINE; } // Gordon: 25/10/02: adding ability to set pliers else if ( !Q_stricmp( weaponName, "PLIERS" ) ) { weapon = WP_PLIERS; } else if ( !Q_stricmp( weaponName, "DYNAMITE" ) ) { weapon = WP_DYNAMITE; } else if ( !Q_stricmp( weaponName, "LANDMINE" ) ) { weapon = WP_LANDMINE; } else if ( !Q_stricmp( weaponName, "STEN" ) ) { weapon = WP_STEN; } else if ( !Q_stricmp( weaponName, "PANZERFAUST" ) ) { weapon = WP_PANZERFAUST; } else if ( !Q_stricmp( weaponName, "MORTAR" ) ) { weapon = WP_MORTAR; } else if ( !Q_stricmp( weaponName, "MORTAR_DEPLOYED" ) ) { weapon = WP_MORTAR_SET; } else if ( !Q_stricmp( weaponName, "FLAMETHROWER" ) ) { weapon = WP_FLAMETHROWER; } else if ( !Q_stricmp( weaponName, "FG42" ) ) { weapon = WP_FG42; } else if ( !Q_stricmp( weaponName, "MOBILE_MG42" ) ) { weapon = WP_MOBILE_MG42; } else if ( !Q_stricmp( weaponName, "SYRINGE" ) ) { weapon = WP_MEDIC_SYRINGE; } else if ( !Q_stricmp( weaponName, "MEDKIT" ) ) { weapon = WP_MEDKIT; } else if ( !Q_stricmp( weaponName, "K43" ) ) { weapon = WP_K43; } else if ( !Q_stricmp( weaponName, "GARAND" ) ) { weapon = WP_GARAND; } else if ( !Q_stricmp( weaponName, "SMOKEBOMB" ) ) { weapon = WP_SMOKE_BOMB; } else if ( !Q_stricmp( weaponName, "SATCHEL" ) ) { weapon = WP_SATCHEL; } else if ( !Q_stricmp( weaponName, "AMMOKIT" ) ) { weapon = WP_AMMO; } else if ( !Q_stricmp( weaponName, "NONE" ) ) { weapon = WP_NONE; } else if ( !Q_stricmp( weaponName, "KNIFE" ) ) { weapon = WP_KNIFE; } else if ( !Q_stricmp( weaponName, "LUGER" ) ) { weapon = WP_LUGER; } else if ( !Q_stricmp( weaponName, "COLT" ) ) { weapon = WP_COLT; } else { return -1; } if ( BG_CanUseWeapon( classNum, teamNum, weapon ) ) { return weapon; } else { return -1; } } /* =============== G_AddBot =============== */ #define MAX_SLOT_NUMBER 6 /* ================================= Find the next free slot number (for allies bot) ================================= */ #if 0 // rain - unused static void G_AssignBotSlot( gentity_t *bot ) { int i; gentity_t *ent; int slotUsed[MAX_SLOT_NUMBER]; for ( i = 0; i < MAX_SLOT_NUMBER; i++ ) slotUsed[i] = qfalse; for ( i = 0; i < MAX_CLIENTS; i++ ) { ent = g_entities + i; if ( ent->client && ent != bot && ent->r.svFlags & SVF_BOT && ent->client->sess.sessionTeam == TEAM_ALLIES ) { if ( ent->client->botSlotNumber > 0 ) { slotUsed[ent->client->botSlotNumber - 1] = qtrue; } else { G_Error( "Buddy %s is not assigned to any number", ent->client->pers.netname ); } } } for ( i = 0; i < MAX_SLOT_NUMBER; i++ ) { if ( !slotUsed[i] ) { bot->client->botSlotNumber = i + 1; // slot number is non-0 return; } } G_Error( "Maximum number of allies buddies exceeded" ); } #endif static void G_AddBot( const char *name, int skill, const char *team, const char *spawnPoint, int playerClass, int playerWeapon, int characerIndex, const char *respawn, const char *scriptName, int rank, int skills[], qboolean pow ) { #define MAX_BOTNAMES 1024 int clientNum; char *botinfo; gentity_t *bot; char *key; char *s; char *botname; // char *model; char userinfo[MAX_INFO_STRING]; // get the botinfo from bots.txt botinfo = G_GetBotInfoByName( "wolfbot" ); if ( !botinfo ) { G_Printf( S_COLOR_RED "Error: Bot '%s' not defined\n", name ); return; } // create the bot's userinfo userinfo[0] = '\0'; botname = Info_ValueForKey( botinfo, "funname" ); if ( !botname[0] ) { botname = Info_ValueForKey( botinfo, "name" ); } Info_SetValueForKey( userinfo, "name", botname ); Info_SetValueForKey( userinfo, "rate", "25000" ); Info_SetValueForKey( userinfo, "snaps", "20" ); Info_SetValueForKey( userinfo, "skill", va( "%i", skill ) ); s = Info_ValueForKey( botinfo, "aifile" ); if ( !*s ) { trap_Printf( S_COLOR_RED "Error: bot has no aifile specified\n" ); return; } // have the server allocate a client slot clientNum = trap_BotAllocateClient( 0 ); // Arnout: 0 means no prefered clientslot if ( clientNum == -1 ) { G_Printf( S_COLOR_RED "Unable to add bot. All player slots are in use.\n" ); G_Printf( S_COLOR_RED "Start server with more 'open' slots (or check setting of sv_maxclients cvar).\n" ); return; } // initialize the bot settings if ( !team || !*team ) { if ( PickTeam( clientNum ) == TEAM_AXIS ) { team = "red"; } else { team = "blue"; } } Info_SetValueForKey( userinfo, "characterfile", Info_ValueForKey( botinfo, "aifile" ) ); //Info_SetValueForKey( userinfo, "skill", va( "%i", skill ) ); Info_SetValueForKey( userinfo, "team", team ); if ( spawnPoint && spawnPoint[0] ) { Info_SetValueForKey( userinfo, "spawnPoint", spawnPoint ); } if ( scriptName && scriptName[0] ) { Info_SetValueForKey( userinfo, "scriptName", scriptName ); } /* if (playerClass > 0) { Info_SetValueForKey( userinfo, "pClass", va("%i", playerClass) ); } if (playerWeapon) { Info_SetValueForKey( userinfo, "pWeapon", va("%i", playerWeapon) ); }*/ // END Mad Doc - TDF key = "wolfbot"; if ( !Q_stricmp( (char *)name, key ) ) { // read the botnames file, and pick a name that doesnt exist fileHandle_t f; int len, i, j, k; qboolean setname = qfalse; char botnames[8192], *pbotnames, *listbotnames[MAX_BOTNAMES], *token, *oldpbotnames; int lengthbotnames[MAX_BOTNAMES]; len = trap_FS_FOpenFile( "botfiles/botnames.txt", &f, FS_READ ); if ( len >= 0 ) { if ( len > sizeof( botnames ) ) { G_Error( "botfiles/botnames.txt is too big (max = %i)", (int)sizeof( botnames ) ); } memset( botnames, 0, sizeof( botnames ) ); trap_FS_Read( botnames, len, f ); pbotnames = botnames; // read them in i = 0; oldpbotnames = pbotnames; while ( ( token = COM_Parse( &pbotnames ) ) ) { if ( !token[0] ) { break; } listbotnames[i] = strstr( oldpbotnames, token ); lengthbotnames[i] = strlen( token ); listbotnames[i][lengthbotnames[i]] = 0; oldpbotnames = pbotnames; if ( ++i == MAX_BOTNAMES ) { break; } } // if ( i > 2 ) { j = rand() % ( i - 1 ); // start at a random spot inthe list for ( k = j + 1; k != j; k++ ) { if ( k == i ) { k = -1; // gets increased on next loop continue; } if ( ClientFromName( listbotnames[k] ) == -1 ) { // found an unused name Info_SetValueForKey( userinfo, "name", listbotnames[k] ); setname = qtrue; break; } } } // trap_FS_FCloseFile( f ); } if ( !setname ) { Info_SetValueForKey( userinfo, "name", va( "wolfbot_%i", clientNum + 1 ) ); } } else { Info_SetValueForKey( userinfo, "name", name ); } // if a character was specified, put the index of that character filename in the CS_CHARACTERS table in the userinfo if ( characerIndex != -1 ) { Info_SetValueForKey( userinfo, "ch", va( "%i", characerIndex ) ); } // if a rank was specified, use that /* if (rank != -1) { Info_SetValueForKey(userinfo, "rank", va("%i", rank)); }*/ // END Mad Doc - TDF bot = &g_entities[ clientNum ]; bot->r.svFlags |= SVF_BOT; if ( pow ) { bot->r.svFlags |= SVF_POW; } bot->inuse = qtrue; bot->aiName = bot->client->pers.netname; // register the userinfo trap_SetUserinfo( clientNum, userinfo ); // have it connect to the game as a normal client if ( ( s = ClientConnect( clientNum, qtrue, qtrue ) ) ) { G_Printf( S_COLOR_RED "Unable to add bot: %s\n", s ); return; } SetTeam( bot, (char *)team, qtrue, -1, -1, qfalse ); /* if( skills ) { int i; for( i = 0; i < SK_NUM_SKILLS; i++ ) { bot->client->sess.skill[i] = skills[i]; } }*/ return; } /* =============== Svcmd_AddBot_f =============== */ void Svcmd_AddBot_f( void ) { int skill; int delay; char name[MAX_TOKEN_CHARS]; char string[MAX_TOKEN_CHARS]; char team[MAX_TOKEN_CHARS]; // are bots enabled? if ( !bot_enable.integer ) { return; } // name trap_Argv( 1, name, sizeof( name ) ); /// read it just so we can check if it's a name (old method) if ( name[0] && !Q_stricmp( name, "?" ) ) { trap_Printf( "Usage: Addbot [skill 1-4] [team (RED/BLUE)] [msec delay]\n" ); return; } Q_strncpyz( name, "wolfbot", sizeof( name ) ); // RF, hard code the bots for wolf if ( !name[0] ) { trap_Printf( "Usage: Addbot [skill 1-4] [team (RED/BLUE)] [msec delay]\n" ); return; } // skill trap_Argv( 1, string, sizeof( string ) ); if ( !string[0] ) { trap_Cvar_Update( &bot_defaultskill ); skill = bot_defaultskill.integer; } else { skill = atoi( string ); } // team trap_Argv( 2, team, sizeof( team ) ); // delay trap_Argv( 3, string, sizeof( string ) ); if ( !string[0] ) { delay = 0; } else { delay = atoi( string ); } G_AddBot( name, skill, team, NULL, 0, 0, -1, NULL, NULL, -1, NULL, qfalse ); // if this was issued during gameplay and we are playing locally, // go ahead and load the bot's media immediately if ( level.time - level.startTime > 1000 && trap_Cvar_VariableIntegerValue( "cl_running" ) ) { } } typedef struct { char *cmd; char *string; qboolean appendParams; char *help; int count; } spawnBotCommand_t; int G_ClassForString( char *string ) { if ( !Q_stricmp( string, "ANY" ) ) { return -1; } else if ( !Q_stricmp( string, "soldier" ) ) { return PC_SOLDIER; } else if ( !Q_stricmp( string, "medic" ) ) { return PC_MEDIC; } else if ( !Q_stricmp( string, "engineer" ) ) { return PC_ENGINEER; } else if ( !Q_stricmp( string, "lieutenant" ) ) { // FIXME: remove from missionpack return PC_FIELDOPS; } else if ( !Q_stricmp( string, "fieldops" ) ) { return PC_FIELDOPS; } else if ( !Q_stricmp( string, "covertops" ) ) { return PC_COVERTOPS; } else { G_Error( "unknown player class: %s", string ); return -1; //shutup compiler } } void G_BotParseCharacterParms( char *characterFile, int *characterInt ) { if ( strlen( characterFile ) ) { int characterIndex = G_CharacterIndex( characterFile ); if ( characterIndex ) { *characterInt = characterIndex; } else { *characterInt = -1; } } else { *characterInt = -1; } } /* ================== G_SpawnBot ================== */ void G_SpawnBot( const char *text ) { // bot parameters char name[MAX_TOKEN_CHARS] = "wolfBot"; //GS prevent bot health from counting down to 70 (i.e. don't set STAT_MAX_HEALTH = 70) char skill[MAX_TOKEN_CHARS] = "4"; char team[MAX_TOKEN_CHARS] = ""; char pClass[MAX_TOKEN_CHARS] = ""; char pWeapon[MAX_TOKEN_CHARS] = "0"; char spawnPoint[MAX_TOKEN_CHARS] = ""; char respawn[MAX_TOKEN_CHARS] = ""; char scriptName[MAX_TOKEN_CHARS] = "wolfBot"; char characterFile[MAX_TOKEN_CHARS] = ""; // START - Mad Doc - TDF char rank[MAX_TOKEN_CHARS] = ""; char botSkills[MAX_TOKEN_CHARS] = ""; // END Mad Doc - TDF char pow[MAX_TOKEN_CHARS] = "no"; // This is the selection meny index used in SetWolfSpawnWeapons int weaponSpawnNumber = -1; // parsing vars char *token, *pStr, *old_pStr; char cmd[MAX_TOKEN_CHARS], last_cmd[MAX_TOKEN_CHARS]; char cmd_var[MAX_TOKEN_CHARS]; char string[MAX_TOKEN_CHARS]; int j, pClassInt; int characterInt, rankNum; int skills[SK_NUM_SKILLS]; qboolean prisonerOfWar; int teamNum; // parameters spawnBotCommand_t params[] = { {"/name", name, qfalse, "[name]"}, {"/skill", skill, qfalse, "[0-4]"}, {"/team", team, qfalse, "[AXIS/ALLIES]"}, {"/class", pClass, qfalse, "[SOLDIER/MEDIC/LIEUTENANT/ENGINEER/COVERTOPS/FIELDOPS]"}, // FIXME: remove LIEUTENANT from missionpack {"/weapon", pWeapon, qfalse, "[weaponValue]"}, {"/spawnpoint", spawnPoint, qtrue, "[targetname]"}, {"/respawn", respawn, qfalse, "[ON/OFF]"}, {"/scriptName", scriptName, qfalse, "[scriptName]"}, {"/character", characterFile, qfalse, "[character]"}, // START Mad Doc - TDF {"/rank", rank, qfalse, "[rank]"}, {"/skills", botSkills, qfalse, "[botskills]"}, // not really to be exposed to script // END Mad Doc - TDF {"/pow", pow, qfalse, "[yes/no]"}, {NULL} }; // special tables typedef struct { char *weapon; int index; } spawnBotWeapons_t; // TAT 1/16/2003 - uninit'ed data here - getting crazy data for the skills memset( &skills, 0, sizeof( skills ) ); // // parse the vars pStr = (char *)text; token = COM_Parse( &pStr ); Q_strncpyz( cmd, token, sizeof( cmd ) ); // if this is a question mark, show help info if ( !Q_stricmp( cmd, "?" ) || !Q_stricmp( cmd, "/?" ) ) { G_Printf( "Spawns a bot into the game, with the given parameters.\n\nSPAWNBOT [/param [value]] [/param [value]] ...\n\n where [/param [value]] may consist of:\n\n" ); for ( j = 0; params[j].cmd; j++ ) { G_Printf( " %s %s\n", params[j].cmd, params[j].help ); } return; } // // intitializations for ( j = 0; params[j].cmd; j++ ) { params[j].count = 0; } memset( last_cmd, 0, sizeof( last_cmd ) ); pStr = (char *)text; // // parse each command while ( cmd[0] ) { // // build the string up to the next parameter change token = COM_Parse( &pStr ); Q_strncpyz( cmd, token, sizeof( cmd ) ); if ( !cmd[0] ) { break; } // if we find an "or", then use the last command if ( !Q_stricmp( cmd, "or" ) ) { // use the last command Q_strncpyz( cmd, last_cmd, sizeof( cmd ) ); } // // read the parameters memset( string, 0, sizeof( string ) ); token = COM_Parse( &pStr ); Q_strncpyz( cmd_var, token, sizeof( cmd_var ) ); if ( !cmd_var[0] ) { break; } while ( qtrue ) { Q_strcat( string, sizeof( string ), cmd_var ); old_pStr = pStr; token = COM_Parse( &pStr ); Q_strncpyz( cmd_var, token, sizeof( cmd_var ) ); if ( cmd_var[0] && ( cmd_var[0] == '/' || !Q_stricmp( cmd_var, "or" ) ) ) { pStr = old_pStr; break; } if ( !cmd_var[0] ) { break; } Q_strcat( string, sizeof( string ), " " ); } // // see if this command exists in the parameters table for ( j = 0; params[j].cmd; j++ ) { if ( !Q_stricmp( params[j].cmd, cmd ) ) { // found a match, if this field already has an entry, then randomly override it if ( !params[j].count || ( !params[j].appendParams && ( ( rand() % ( ++params[j].count ) ) == 0 ) ) ) { Q_strncpyz( params[j].string, string, sizeof( string ) ); } else if ( params[j].appendParams ) { // append this token Q_strcat( params[j].string, sizeof( string ), va( " %s", string ) ); } params[j].count++; break; } } if ( !params[j].cmd ) { G_Printf( "G_SpawnBot: unknown parameter: %s\nFor usage info, use \"spawnbot /?\"\n", cmd ); return; } // Q_strncpyz( last_cmd, cmd, sizeof( last_cmd ) ); Q_strncpyz( cmd, cmd_var, sizeof( cmd ) ); } // if ( strlen( pClass ) ) { pClassInt = 1 + G_ClassForString( pClass ); } else { pClassInt = 0; } if ( !Q_stricmp( team, "red" ) || !Q_stricmp( team, "r" ) || !Q_stricmp( team, "axis" ) ) { teamNum = TEAM_AXIS; } else if ( !Q_stricmp( team, "blue" ) || !Q_stricmp( team, "b" ) || !Q_stricmp( team, "allies" ) ) { teamNum = TEAM_ALLIES; } else { // pick the team with the least number of players teamNum = PickTeam( -1 ); } G_BotParseCharacterParms( characterFile, &characterInt ); // Gordon: 27/11/02 if ( *pow && !Q_stricmp( pow, "yes" ) ) { prisonerOfWar = qtrue; } else { prisonerOfWar = qfalse; } // START Mad Doc - TDF // special case: if "NONE" is specified, treat this differently if ( !Q_stricmp( pWeapon, "NONE" ) ) { weaponSpawnNumber = -1; } // END Mad Doc - TDF // START Mad Doctor I changes, 8/17/2002. // If we have a weapon specified, and we have a class specified else if ( isdigit( pWeapon[0] ) ) { // Just convert the string to a number weaponSpawnNumber = atoi( pWeapon ); } // if (isdigit(pWeapon[0]))... // If we have a weapon specified as a string, and we have a class specified else if ( pClassInt > 0 ) { // Translate the weapon name into a proper weapon index // Get the index for the weapon weaponSpawnNumber = Bot_GetWeaponForClassAndTeam( pClassInt - 1, teamNum, pWeapon ); // Get default weapon if ( weaponSpawnNumber == -1 ) { weaponSpawnNumber = BG_GetPlayerClassInfo( teamNum, pClassInt - 1 )->classWeapons[0]; } } // if (Q_stricmp(pWeapon[MAX_TOKEN_CHARS], "0")... // Otherwise, no weapon is selected else { // Just use the default weaponSpawnNumber = BG_GetPlayerClassInfo( teamNum, pClassInt - 1 )->classWeapons[0]; } // else... // START Mad Doc - TDF rankNum = atoi( rank ); if ( rankNum ) { rankNum--; // people like to start with 1 // Gordon: coders are people too :( } if ( botSkills[0] ) { // parse the skills out int i; char *pString, *token; pString = botSkills; for ( i = 0; i < SK_NUM_SKILLS; i++ ) { token = COM_ParseExt( &pString, qfalse ); skills[i] = atoi( token ); } } // {"/botskills", botSkills, qfalse, "[botskills]"}, // not really to be exposed to script // END Mad Doc - TDF G_AddBot( name, atoi( skill ), team, spawnPoint, pClassInt, weaponSpawnNumber, characterInt, respawn, scriptName, rankNum, skills, prisonerOfWar ); // END Mad Doctor I changes, 8/17/2002. } /* ============== Svcmd_SpawnBot ============== */ void Svcmd_SpawnBot() { char text[1024]; char cmd[MAX_TOKEN_CHARS]; int i; // build the string memset( text, 0, sizeof( text ) ); for ( i = 1; i < trap_Argc(); i++ ) { trap_Argv( i, cmd, sizeof( cmd ) ); // if ( i > 1 ) { Q_strcat( text, sizeof( text ), " " ); } // if ( strchr( cmd, ' ' ) ) { Q_strcat( text, sizeof( text ), "\"" ); } // Q_strcat( text, sizeof( text ), cmd ); // if ( strchr( cmd, ' ' ) ) { Q_strcat( text, sizeof( text ), "\"" ); } } G_SpawnBot( text ); } // TDF - Mad Doc /* ============== G_RemoveNamedBot - removes the bot named "name" ============== */ int G_RemoveNamedBot( char *name ) { int i; char netname[36]; gclient_t *cl; for ( i = 0 ; i < g_maxclients.integer ; i++ ) { cl = level.clients + i; if ( cl->pers.connected != CON_CONNECTED ) { continue; } if ( !( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) ) { continue; } // use scriptname, not netname if ( !Q_stricmp( name, cl->pers.botScriptName ) ) { // found the bot. boot him strcpy( netname, cl->pers.netname ); Q_CleanStr( netname ); trap_SendConsoleCommand( EXEC_INSERT, va( "kick \"%s\" 0\n", netname ) ); return qtrue; } } return qfalse; } /* TDF - Mad Doc =============== Svcmd_RemoveBot_f =============== */ void Svcmd_RemoveBot_f( void ) { char name[MAX_TOKEN_CHARS]; // are bots enabled? if ( !bot_enable.integer ) { return; } // name trap_Argv( 1, name, sizeof( name ) ); /// read it just so we can check if it's a name (old method) if ( !name[0] ) { trap_Printf( "Usage: Removebot name\n" ); return; } G_RemoveNamedBot( name ); } /* =============== G_LoadBots =============== */ static void G_LoadBots( void ) { int len; char *filename; vmCvar_t botsFile; fileHandle_t f; char buf[MAX_BOTS_TEXT]; if ( !bot_enable.integer ) { return; } trap_Cvar_Register( &botsFile, "g_botsFile", "", CVAR_INIT | CVAR_ROM ); if ( *botsFile.string ) { filename = botsFile.string; } else { filename = "scripts/bots.txt"; } len = trap_FS_FOpenFile( filename, &f, FS_READ ); if ( !f ) { trap_Printf( va( S_COLOR_RED "file not found: %s\n", filename ) ); return; } if ( len >= MAX_BOTS_TEXT ) { trap_Printf( va( S_COLOR_RED "file too large: %s is %i, max allowed is %i", filename, len, MAX_BOTS_TEXT ) ); trap_FS_FCloseFile( f ); return; } trap_FS_Read( buf, len, f ); buf[len] = 0; trap_FS_FCloseFile( f ); g_numBots = Com_ParseInfos( buf, MAX_BOTS, g_botInfos ); trap_Printf( va( "%i bots parsed\n", g_numBots ) ); // load bot script Bot_ScriptLoad(); } /* =============== G_GetBotInfoByNumber =============== */ char *G_GetBotInfoByNumber( int num ) { if ( num < 0 || num >= g_numBots ) { trap_Printf( va( S_COLOR_RED "Invalid bot number: %i\n", num ) ); return NULL; } return g_botInfos[num]; } /* =============== G_GetBotInfoByName =============== */ char *G_GetBotInfoByName( const char *name ) { int n; char *value; for ( n = 0; n < g_numBots ; n++ ) { value = Info_ValueForKey( g_botInfos[n], "name" ); if ( !Q_stricmp( value, name ) ) { return g_botInfos[n]; } } return NULL; } void BotBuildStaticEntityCache( void ); void BotCalculateMg42Spots( void ); /* ================= G_BotDelayedInit ================= */ void G_BotDelayedInit( gentity_t *ent ) { G_FreeEntity( ent ); BotBuildStaticEntityCache(); // are there explosiveTargets? level.explosiveTargets[0] = GetTargetExplosives( TEAM_AXIS, qtrue ); level.explosiveTargets[1] = GetTargetExplosives( TEAM_ALLIES, qtrue ); // calculate mg42 spots BotCalculateMg42Spots(); } /* =============== G_InitBots =============== */ void G_InitBots( qboolean restart ) { G_LoadBots(); trap_Cvar_Register( &bot_debug, "bot_debug", "0", 0 ); // the static entity cache is not ready yet level.initStaticEnts = qfalse; // determine which team is attacking level.captureFlagMode = qfalse; return; } //============================================================================================ extern vec3_t playerMins, playerMaxs; /* =============== BotDropToFloor =============== */ void BotDropToFloor( gentity_t *ent ) { vec3_t dest; trace_t tr; vec3_t checkMins, checkMaxs; //----(SA) move the bounding box for the check in 1 unit on each side so they can butt up against a wall and not startsolid VectorCopy( playerMins, checkMins ); checkMins[0] += 1; checkMins[1] += 1; VectorCopy( playerMaxs, checkMaxs ); checkMaxs[0] -= 1; checkMaxs[1] -= 1; // use low max Z in case this is a crouch spot checkMaxs[2] = 0; // drop to floor ent->r.currentOrigin[2] += 1.0; // fixes QErad -> engine bug? VectorSet( dest, ent->r.currentOrigin[0], ent->r.currentOrigin[1], ent->r.currentOrigin[2] - 4096 ); trap_Trace( &tr, ent->r.currentOrigin, checkMins, checkMaxs, dest, ent->s.number, MASK_PLAYERSOLID ); if ( tr.startsolid ) { // try raising us up some if ( fabs( ent->r.currentOrigin[2] - ent->s.origin[2] ) < 48 ) { ent->r.currentOrigin[2] += 4; BotDropToFloor( ent ); return; } G_Printf( "WARNING: %s (%s) in solid at %s\n", ent->classname, ent->targetname, vtos( ent->r.currentOrigin ) ); return; } G_SetOrigin( ent, tr.endpos ); VectorCopy( ent->r.currentOrigin, ent->s.origin ); } // Equivalent of BotDropToFloor for server entities void ServerEntityDropToFloor( g_serverEntity_t *ent ) { vec3_t dest; trace_t tr; vec3_t checkMins, checkMaxs; //----(SA) move the bounding box for the check in 1 unit on each side so they can butt up against a wall and not startsolid VectorCopy( playerMins, checkMins ); checkMins[0] += 1; checkMins[1] += 1; VectorCopy( playerMaxs, checkMaxs ); checkMaxs[0] -= 1; checkMaxs[1] -= 1; // use low max Z in case this is a crouch spot checkMaxs[2] = 0; // drop to floor ent->origin[2] += 1.0; // fixes QErad -> engine bug? VectorSet( dest, ent->origin[0], ent->origin[1], ent->origin[2] - 4096 ); trap_Trace( &tr, ent->origin, checkMins, checkMaxs, dest, -1, MASK_PLAYERSOLID ); if ( tr.startsolid ) { // try raising us up some if ( fabs( ent->origin[2] - ent->origin[2] ) < 48 ) { ent->origin[2] += 4; ServerEntityDropToFloor( ent ); return; } G_Printf( "WARNING: %s (%s) in solid at %s\n", ent->classname, ent->name, vtos( ent->origin ) ); return; } VectorCopy( tr.endpos, ent->origin ); } /*QUAKED ai_marker (1 0.5 0) (-18 -18 -24) (18 18 48) NODROP CROUCHING AI marker NODROP means dont drop it to the ground "targetname" : identifier for this marker */ // TAT 11/18/2002 - Use this function as the setup func for ai_markers that are NOT generated from q3map, but are parsed from the external file void SP_AIMarker_Setup( g_serverEntity_t *ent ) { // since we didn't spawn, we haven't done this yet if ( !( ent->spawnflags & 1 ) ) { ServerEntityDropToFloor( ent ); } } void SP_ai_marker( gentity_t *ent ) { if ( !( ent->spawnflags & 1 ) ) { BotDropToFloor( ent ); } // TAT 11/13/2002 - use the server entities for this CreateServerEntity( ent ); // free this entity - we should now have a server entity for it G_FreeEntity( ent ); } /*QUAKED bot_sniper_spot (1 0.2 0) (-18 -18 -24) (18 18 48) NODROP CROUCHING Bots will use this spot to snipe from NODROP means dont drop it to the ground "aiTeam" team to use this spot, if 0, any team can use it 1 = AXIS, 2 = ALLIES */ void SP_bot_sniper_spot( gentity_t *ent ) { if ( !( ent->spawnflags & 1 ) ) { BotDropToFloor( ent ); } ent->r.svFlags |= SVF_NOCLIENT; ent->s.eType = ET_SNIPER_HINT; } /*QUAKED bot_landminespot_spot (1 0.2 0) (-18 -18 -24) (18 18 48) NODROP CROUCHING Bots will use this spot to search for landmines from, target at a info_null NODROP means dont drop it to the ground "aiTeam" team to use this spot, if 0, any team can use it 1 = AXIS, 2 = ALLIES */ void bot_landminespot_setup( gentity_t* self ) { gentity_t* target = G_FindByTargetname( NULL, self->target ); if ( !target ) { G_FreeEntity( self ); return; } VectorCopy( target->s.origin, self->s.origin2 ); } void SP_bot_landminespot_spot( gentity_t* ent ) { // MUST have a target if ( !ent->target || !*ent->target ) { G_FreeEntity( ent ); return; } if ( !( ent->spawnflags & 1 ) ) { BotDropToFloor( ent ); } ent->r.svFlags |= SVF_NOCLIENT; ent->s.eType = ET_LANDMINESPOT_HINT; ent->think = bot_landminespot_setup; ent->nextthink = level.time + FRAMETIME; } /*QUAKED bot_attractor (1 0.2 0) (-18 -18 -24) (18 18 48) NODROP Bots will defend this general area when enabled as a goal NODROP means dont drop it to the ground */ void SP_bot_attractor( gentity_t *ent ) { if ( !( ent->spawnflags & 1 ) ) { BotDropToFloor( ent ); } ent->r.svFlags |= SVF_NOCLIENT; ent->s.eType = ET_ATTRACTOR_HINT; } /*QUAKED bot_jump_source (0.5 0.8 0) (-18 -18 -24) (18 18 40) NODROP Bots will use this spot to jump from Must recompile .AAS file after adding/changing these entities NOTE: MUST HAVE A MATCHING BOT_JUMP_DEST NODROP means dont drop it to the ground */ void SP_bot_jump_source( gentity_t *ent ) { // only used for bspc process G_FreeEntity( ent ); } /*QUAKED bot_jump_dest (0.5 0.8 0) (-18 -18 -24) (18 18 40) NODROP Bots will use this spot to jump to Must recompile .AAS file after adding/changing these entities NOTE: MUST HAVE A MATCHING BOT_JUMP_SOURCE NODROP means dont drop it to the ground */ void SP_bot_jump_dest( gentity_t *ent ) { // only used for bspc process G_FreeEntity( ent ); } /*QUAKED bot_seek_cover_spot (0 0.6 0) (-18 -18 -24) (18 18 48) NODROP CROUCHING CROUCH_TOWARDS PRONING PRONE_TOWARDS EXPOSED NEVER_PRONE Allies bots will use this spot to seek cover while in follow formation Join these together in sequence of player progression for best results !!NOTE!! : each seek_cover_spot can only have ONE parent. A parent can have multiple children, but you cannot point two spots to the same destination. NODROP means dont drop it to the ground CROUCHING means crouch when bot gets there CROUCH_TOWARD means crouch all the way to the spot (within a resonable distance) PRONING means prone when bot gets there PRONE_TOWARD means prone all the way to the spot (within a reasonable distance) EXPOSED means this spot is in the open, so flee from it easier NEVER_PRONE means bots should never go prone at this spot "angle" is the direction the bot should face when they reach the spot "target" next marker(s) in sequence "targetname" our identifier for sequencing */ void bot_seek_cover_spot_think( g_serverEntity_t *ent ) { g_serverEntity_t *trav, *lastTrav; // if ( ent->name ) { // find our parent trav = NULL; while ( ( trav = FindServerEntity( trav, SE_FOFS( target ), ent->name ) ) ) { if ( !Q_stricmp( trav->classname, ent->classname ) ) { ent->parent = trav; break; } } } // Don't bother looking for a target if none is specified. Mad Doctor I if ( ent->target && ( ent->target[0] != 0 ) ) { // // now find our ->target_ent, if we have multiple targets, then use ->chain trav = NULL; lastTrav = NULL; while ( ( trav = FindServerEntity( trav, SE_FOFS( name ), ent->target ) ) ) { if ( Q_stricmp( trav->classname, ent->classname ) ) { G_Error( "bot_seek_cover_spot at %s is targetting a %s", vtos( ent->origin ), trav->classname ); } if ( !ent->target_ent ) { ent->target_ent = trav; //VectorSubtract( trav->s.origin, ent->s.origin, ent->movedir ); //VectorNormalize( ent->movedir ); } if ( lastTrav ) { lastTrav->chain = trav; } lastTrav = trav; } } // if (ent->target[0] != 0)... } // TAT 11/18/2002 - Use this function as the setup func for cover spots that are NOT generated from q3map, but are parsed from the external file void SP_SeekCover_Setup( g_serverEntity_t *ent ) { // since we didn't spawn, we haven't done this yet if ( !( ent->spawnflags & 1 ) ) { ServerEntityDropToFloor( ent ); } // now do the normal thing bot_seek_cover_spot_think( ent ); } void SP_Seek_Cover_Spawn( gentity_t *ent, int team ) { g_serverEntity_t *svEnt; if ( !( ent->spawnflags & 1 ) ) { BotDropToFloor( ent ); } ent->aiTeam = team; if ( ent->targetname ) { // TAT 11/13/2002 - seek cover spots are special server only entities // so let's make one with our data svEnt = CreateServerEntity( ent ); // set the setup func svEnt->setup = bot_seek_cover_spot_think; } // free this entity - we should now have a server entity for it G_FreeEntity( ent ); } void SP_bot_seek_cover_spot( gentity_t *ent ) { // for allied bots only SP_Seek_Cover_Spawn( ent, 2 ); } /*QUAKED bot_axis_seek_cover_spot (0.6 0.6 0.8) (-18 -18 -24) (18 18 48) NODROP CROUCHING CROUCH_TOWARDS PRONING PRONE_TOWARDS EXPOSED NEVER_PRONE Axis bots will use this spot to seek cover while fighting !!NOTE!! : each axis_seek_cover_spot can only have ONE parent. A parent can have multiple children, but you cannot point two spots to the same destination. NODROP means dont drop it to the ground CROUCHING means crouch when bot gets there CROUCH_TOWARD means crouch all the way to the spot (within a resonable distance) PRONING means prone when bot gets there PRONE_TOWARD means prone all the way to the spot (within a reasonable distance) EXPOSED means this spot is in the open, so flee from it easier NEVER_PRONE means bots should never go prone at this spot "angle" is the direction the bot should face when they reach the spot "target" next marker(s) in sequence "targetname" our identifier for sequencing */ void SP_bot_axis_seek_cover_spot( gentity_t *ent ) { // for axis bots only SP_Seek_Cover_Spawn( ent, 1 ); } /*QUAKED bot_seek_cover_sequence (1 0.2 0) ? Point this to the bot_seek_cover_spot that should be used for following or bot_axis_seek_cover_spot for axis bots to use target : targetname of the spot to start the sequence. can point to multiple spots. */ void bot_seek_cover_sequence_touch( gentity_t *self, gentity_t *other, trace_t *trace ) { // if we have a seek cover spot, then set the guy who stepped on me to be using it if ( self->serverEntity ) { other->teamchain = self; } else { // stop using seek_cover_spots other->teamchain = NULL; } } void bot_seek_cover_sequence_init( gentity_t *ent ) { g_serverEntity_t *trav, *lastTrav; // if ( !ent->target || !ent->target[0] ) { return; } // find our list of spots trav = NULL; // the first one is different, since we're getting the sequence (a gentity_t) to point at the first spot (a g_serverEntity_t) trav = FindServerEntity( trav, SE_FOFS( name ), ent->target ); ent->serverEntity = trav; // now we just want serverentities pointing at each other lastTrav = trav; while ( ( trav = FindServerEntity( trav, SE_FOFS( name ), ent->target ) ) ) { lastTrav->nextServerEntity = trav; lastTrav = trav; } // // we didn't find anything if ( ent->serverEntity == NULL ) { G_Error( "bot_seek_cover_sequence has no matching spots (\"target\" = \"%s\"", ent->target ); } // lastTrav->nextServerEntity = NULL; } extern void InitTrigger( gentity_t *self ); // Arnout: this was missing void SP_bot_seek_cover_sequence( gentity_t *ent ) { ent->touch = bot_seek_cover_sequence_touch; ent->think = bot_seek_cover_sequence_init; ent->nextthink = level.time + 500; InitTrigger( ent ); trap_LinkEntity( ent ); } /*QUAKED bot_landmine_area (0.5 0.7 0.3) ? AXIS_ONLY ALLIED_ONLY Bots will attempt to place landmines in this area. "count" - number of landmines to place in this area. Defaults to 2. "targetname" - used only to set bot goal-state from within scripting */ void SP_bot_landmine_area( gentity_t *ent ) { char *spawnString; G_SpawnString( "count", "2", &spawnString ); ent->count = atoi( spawnString ); trap_SetBrushModel( ent, ent->model ); ent->r.contents = 0; // replaces the -1 from trap_SetBrushModel ent->r.svFlags = SVF_NOCLIENT; ent->s.eType = ET_LANDMINE_HINT; trap_LinkEntity( ent ); }