Bot fixes.

This commit is contained in:
Justin Marshall
2026-05-20 09:10:15 -07:00
parent ed6b826d23
commit 31a3805769
9 changed files with 571 additions and 421 deletions
+23 -3
View File
@@ -9,7 +9,7 @@ idCVar bot_goaldist( "bot_goaldist", "20", CVAR_INTEGER | CVAR_CHEAT, "" );
idCVar bot_debugnav( "bot_debugnav", "0", CVAR_BOOL | CVAR_CHEAT, "draws navmesh paths for the bot" );
idCVar bot_showstate( "bot_showstate", "0", CVAR_BOOL | CVAR_CHEAT, "draws the bot state above the bot" );
idCVar bot_debug( "bot_debug", "0", CVAR_BOOL, "shows debug info for the bot" );
idCVar bot_skill("bot_skill", "3", CVAR_INTEGER, "");
idCVar bot_skill("bot_skill", "4", CVAR_INTEGER, "");
static int BotSafeAmmoCount( const idInventory& inv, const char* ammoName ) {
@@ -161,7 +161,16 @@ void rvmBot::Spawn( void )
}
// Load in the bot character.
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), 1 );
const int skill = idMath::ClampInt( 1, 5, bot_skill.GetInteger() );
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), skill );
if( bs.character == NULL && skill != 4 )
{
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), 4 );
}
if( bs.character == NULL && skill != 1 )
{
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), 1 );
}
if( !bs.character )
{
gameLocal.Error( "Failed to load character file for bot %s\n", botName.c_str() );
@@ -197,10 +206,12 @@ void rvmBot::Spawn( void )
bs.entitynum = entityNumber;
bs.setupcount = 4;
bs.entergame_time = Bot_Time();
bs.weaponnum = spawnArgs.GetInt( "current_weapon", "1" );
hasSpawned = true;
bs.botinput.respawn = true;
bs.botinput.weapon = bs.weaponnum;
bs.botinput.lastWeaponNum = -1;
stateThread.SetState("state_Respawn");
@@ -316,6 +327,15 @@ void rvmBot::ServerThink( void )
return;
}
if( bs.enemy >= 0 && bs.botinput.speed > 0.0f )
{
bs.viewangles = bs.botinput.viewangles;
bs.useRandomPosition = false;
bs.attackerEntity = NULL;
bs.botinput.weapon = bs.weaponnum;
return;
}
idVec3 goalOrigin = bs.useRandomPosition ? bs.random_move_position : bs.currentGoal.origin;
idVec3 moveGoal = goalOrigin;
bool haveMoveGoal = false;
@@ -508,4 +528,4 @@ void rvmBot::Think( void )
deltaViewAngles.Zero();
idPlayer::Think();
}
}
+9 -5
View File
@@ -132,7 +132,7 @@ class rvmBotAIBotActionBase;
#define ENEMY_HEIGHT 201
#define MAX_AVOIDGOALS 256
#define MAX_GOALSTACK 8
#define MAX_GOALSTACK 16
#define GFL_NONE 0
#define GFL_ITEM 1
@@ -347,7 +347,7 @@ struct weightconfig_t
#define DEFAULT_CHARACTER "bots/default_c.c"
#define MAX_AVOIDGOALS 256
#define MAX_GOALSTACK 8
#define MAX_GOALSTACK 16
#define GFL_NONE 0
#define GFL_ITEM 1
@@ -747,11 +747,11 @@ struct bot_state_t
entitynum = 0;
setupcount = 0;
entergame_time = 0;
weaponnum = 0;
weaponnum = -1;
lasthealth = 0;
ltg_time = 0;
weaponchange_time = 0;
enemy = 0;
enemy = -1;
enemyvisible_time = 0;
enemysuicide = 0;
enemysight_time = 0;
@@ -872,6 +872,10 @@ private:
bool HasWeapon( int index )
{
if( index < 0 || index >= MAX_WEAPONS )
{
return false;
}
return inventory.weapons & ( 1 << index );
}
private:
@@ -923,4 +927,4 @@ private:
idAAS* aas;
};
extern idCVar bot_skill;
extern idCVar bot_skill;
File diff suppressed because it is too large Load Diff
+42 -2
View File
@@ -113,8 +113,48 @@ stateResult_t rvmBot::state_BattleFight(stateParms_t* parms)
}
}
// Move randomly around our AAS area only when there is no useful nearby pickup.
BotMoveInRandomDirection(&bs);
// Fight like a deathmatch bot: keep pressure on the enemy while changing
// strafe direction often enough to be hard to track.
if( bs.attackstrafe_time < Bot_Time() )
{
bs.attackstrafe_time = Bot_Time() + idMath::FRandRange( 0.65f, 1.35f );
if( rvmBotUtil::random() < 0.5f )
{
bs.flags ^= BFL_STRAFERIGHT;
}
}
idVec3 toEnemy = target - bs.origin;
toEnemy[2] = 0.0f;
const float enemyDist = toEnemy.Length();
if( enemyDist > 1.0f )
{
toEnemy /= enemyDist;
idVec3 strafeDir = ( bs.flags & BFL_STRAFERIGHT ) ? idVec3( -toEnemy[1], toEnemy[0], 0.0f ) : idVec3( toEnemy[1], -toEnemy[0], 0.0f );
idVec3 moveDir = strafeDir;
if( enemyDist > 520.0f )
{
moveDir += toEnemy * 0.65f;
}
else if( enemyDist < 170.0f )
{
moveDir -= toEnemy * 0.75f;
}
moveDir.Normalize();
bs.botinput.dir = moveDir;
bs.botinput.speed = pm_runspeed.GetInteger();
if( enemyDist < 420.0f && rvmBotUtil::random() < 0.025f )
{
bs.botinput.actionflags |= ACTION_JUMP;
}
}
else
{
BotMoveInRandomDirection(&bs);
}
//aim at the enemy
BotAimAtEnemy( &bs );
+1 -2
View File
@@ -259,8 +259,7 @@ int idBotCharacterStatsManager::CheckCharacteristicIndex( bot_character_t* ch, i
{
if( ch == NULL )
{
gameLocal.Error( "NULL bot character
" );
gameLocal.Error( "NULL bot character" );
return false;
}
if( index < 0 || index >= MAX_CHARACTERISTICS )
+112 -103
View File
@@ -398,49 +398,50 @@ itemconfig_t* idBotGoalManager::LoadItemConfig( char* filename )
idBotGoalManager::ItemWeightIndex
========================
*/
/*
================
Bot_ItemWeightAlias
Some itemconfig entries come from Quake 3 / Team Arena style bot data, but the
Doom 3 weight config usually does not contain every one of those classnames.
The item weight index must still point at a valid fuzzy weight, so we alias
unsupported / missing items to the closest useful existing Doom 3 weight.
================
*/
static const char* Bot_ItemWeightAlias(const char* classname) {
if (!classname || !classname[0]) {
static const char* Bot_ItemWeightAlias( const char* classname )
{
if( classname == NULL || classname[0] == '\0' )
{
return NULL;
}
struct itemWeightAlias_t {
struct itemWeightAlias_t
{
const char* missingName;
const char* fallbackName;
};
static const itemWeightAlias_t aliases[] = {
// Missing weapon names commonly seen in Q3 bot item configs.
{ "weapon_fists", "weapon_pistol" },
{ "weapon_grapplinghook", "weapon_chainsaw" },
{ "weapon_grenadelauncher", "weapon_rocketlauncher" },
{ "weapon_lightning", "weapon_plasmagun" },
{ "weapon_machinegun", "weapon_machinegun" },
{ "weapon_railgun", "weapon_rocketlauncher" },
{ "weapon_nailgun", "weapon_chaingun" },
{ "weapon_prox_launcher", "weapon_rocketlauncher" },
static const itemWeightAlias_t aliases[] =
{
{ "weapon_machinegun_mp", "weapon_machinegun" },
{ "weapon_shotgun_mp", "weapon_shotgun" },
{ "weapon_rocketlauncher_mp", "weapon_rocketlauncher" },
{ "weapon_plasmagun_mp", "weapon_plasmagun" },
{ "weapon_bfg_mp", "weapon_bfg" },
{ "weapon_grenadelauncher", "weapon_rocketlauncher" },
{ "weapon_grenadelauncher_mp", "weapon_rocketlauncher" },
{ "weapon_lightning", "weapon_plasmagun" },
{ "weapon_lightning_mp", "weapon_plasmagun" },
{ "weapon_railgun", "weapon_rocketlauncher" },
{ "weapon_railgun_mp", "weapon_rocketlauncher" },
{ "weapon_fists", "weapon_pistol" },
{ "weapon_grapplinghook", "weapon_chainsaw" },
{ "weapon_nailgun", "weapon_chaingun" },
{ "weapon_nailgun_mp", "weapon_chaingun" },
{ "weapon_prox_launcher", "weapon_rocketlauncher" },
{ "weapon_prox_launcher_mp", "weapon_rocketlauncher" },
// Team Arena / CTF objective entities. These are not normal pickup goals.
// Give them a valid low-risk fallback so the index is never invalid.
{ "team_redobelisk", "item_armor_shard" },
{ "team_blueobelisk", "item_armor_shard" },
{ "team_neutralobelisk", "item_armor_shard" },
{ "team_redobelisk", "item_armor_shard" },
{ "team_blueobelisk", "item_armor_shard" },
{ "team_neutralobelisk", "item_armor_shard" },
{ NULL, NULL }
};
for (int i = 0; aliases[i].missingName != NULL; i++) {
if (idStr::Icmp(classname, aliases[i].missingName) == 0) {
for( int i = 0; aliases[i].missingName != NULL; i++ )
{
if( idStr::Icmp( classname, aliases[i].missingName ) == 0 )
{
return aliases[i].fallbackName;
}
}
@@ -448,81 +449,84 @@ static const char* Bot_ItemWeightAlias(const char* classname) {
return NULL;
}
/*
================
idBotGoalManager::ItemWeightIndex
================
*/
int* idBotGoalManager::ItemWeightIndex(weightconfig_t* iwc, itemconfig_t* ic) {
int* index;
int i;
if (iwc == NULL || ic == NULL || ic->numiteminfo <= 0) {
return NULL;
static bool Bot_ItemClassnamesMatch( const char* entityClassname, const char* itemClassname )
{
if( entityClassname == NULL || itemClassname == NULL )
{
return false;
}
if( idStr::Icmp( entityClassname, itemClassname ) == 0 )
{
return true;
}
// initialize item weight index
index = new int[ic->numiteminfo]; // jmarshall: Get this off of the heap!
for (i = 0; i < ic->numiteminfo; i++) {
const char* classname = ic->iteminfo[i].classname.c_str();
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)classname);
if (index[i] < 0) {
const char* aliasName = Bot_ItemWeightAlias(classname);
if (aliasName != NULL) {
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)aliasName);
if (index[i] >= 0) {
common->Printf(
"Bot item weight alias: item info %d \"%s\" using \"%s\"\n",
i,
classname,
aliasName
);
continue;
}
}
// Last-resort fallback. This keeps the bot goal system from storing
// invalid fuzzy weight indexes even if the expected alias is missing.
static const char* safeFallbacks[] = {
"item_armor_shard",
"item_armor",
"ammo_clip",
"weapon_pistol",
"weapon_shotgun",
"weapon_machinegun",
NULL
};
for (int j = 0; safeFallbacks[j] != NULL; j++) {
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)safeFallbacks[j]);
if (index[i] >= 0) {
common->Printf(
"Bot item weight fallback: item info %d \"%s\" using \"%s\"\n",
i,
classname,
safeFallbacks[j]
);
break;
}
}
if (index[i] < 0) {
common->Warning(
"item info %d \"%s\" has no fuzzy weight and no fallback weight\r\n",
i,
classname
);
}
idStr normalizedItem = itemClassname;
if( idStr::Icmp( normalizedItem.Right( 3 ).c_str(), "_mp" ) == 0 )
{
normalizedItem.StripTrailing( "_mp" );
if( idStr::Icmp( entityClassname, normalizedItem.c_str() ) == 0 )
{
return true;
}
}
const char* aliasName = Bot_ItemWeightAlias( itemClassname );
return aliasName != NULL && idStr::Icmp( entityClassname, aliasName ) == 0;
}
int* idBotGoalManager::ItemWeightIndex( weightconfig_t* iwc, itemconfig_t* ic )
{
int* index;
if( iwc == NULL || ic == NULL || ic->numiteminfo <= 0 )
{
return NULL;
}
//initialize item weight index
index = new int[ic->numiteminfo]; // jmarshall: Get this off of the heap!
for( int i = 0; i < ic->numiteminfo; i++ )
{
const char* classname = ic->iteminfo[i].classname.c_str();
index[i] = botFuzzyWeightManager.FindFuzzyWeight( iwc, ( char* )classname );
if( index[i] < 0 )
{
const char* aliasName = Bot_ItemWeightAlias( classname );
if( aliasName != NULL )
{
index[i] = botFuzzyWeightManager.FindFuzzyWeight( iwc, ( char* )aliasName );
}
}
if( index[i] < 0 )
{
static const char* safeFallbacks[] =
{
"item_armor_shard",
"item_armor",
"ammo_clip",
"weapon_machinegun",
"weapon_shotgun",
NULL
};
for( int j = 0; safeFallbacks[j] != NULL; j++ )
{
index[i] = botFuzzyWeightManager.FindFuzzyWeight( iwc, ( char* )safeFallbacks[j] );
if( index[i] >= 0 )
{
break;
}
}
}
if( index[i] < 0 )
{
common->Warning( "item info %d \"%s\" has no fuzzy weight\r\n", i, classname );
}
}
return index;
}
/*
========================
idBotGoalManager::InitLevelItemHeap
@@ -552,7 +556,7 @@ levelitem_t* idBotGoalManager::AllocLevelItem( void )
li = freelevelitems;
if( !li )
{
gameLocal.Error( "out of level items\n" );
gameLocal.Warning( "out of bot level items, increase MAX_BOT_LEVEL_ITEMS\n" );
return NULL;
}
@@ -751,7 +755,7 @@ void idBotGoalManager::InitLevelItems( void )
for( i = 0; i < ic->numiteminfo; i++ )
{
if( !strcmp( classname, ic->iteminfo[i].classname ) )
if( Bot_ItemClassnamesMatch( classname.c_str(), ic->iteminfo[i].classname.c_str() ) )
{
break;
}
@@ -1517,11 +1521,16 @@ void idBotGoalManager::BotPushGoal( int goalstate, bot_goal_t* goal )
{
return;
}
if( gs->goalstacktop > 0 && gs->goalstack[gs->goalstacktop].number == goal->number )
{
gs->goalstack[gs->goalstacktop] = *goal;
return;
}
if( gs->goalstacktop >= MAX_GOALSTACK - 1 )
{
gameLocal.Error( "goal heap overflow\n" );
gameLocal.Warning( "bot goal stack overflow, clearing stale goals\n" );
BotDumpGoalStack( goalstate );
return;
gs->goalstacktop = 0;
}
gs->goalstacktop++;
gs->goalstack[gs->goalstacktop] = *goal;
+2 -2
View File
@@ -3,7 +3,7 @@
#define MAX_BOT_ITEM_INFOS 2
#define MAX_BOT_ITEM_INFO 256
#define MAX_BOT_LEVEL_ITEMS 256
#define MAX_BOT_LEVEL_ITEMS 2048
//#define DEBUG_AI_GOAL
#ifdef RANDOMIZE
@@ -339,4 +339,4 @@ private:
idList<campspot_t> campspots;
};
extern idBotGoalManager botGoalManager;
extern idBotGoalManager botGoalManager;
+2 -2
View File
@@ -8424,7 +8424,7 @@ static classVariableInfo_t bot_goalstate_t_typeInfo[] = {
{ "int *", "itemweightindex", (intptr_t)(&((bot_goalstate_t *)0)->itemweightindex), sizeof( ((bot_goalstate_t *)0)->itemweightindex ) },
{ "int", "client", (intptr_t)(&((bot_goalstate_t *)0)->client), sizeof( ((bot_goalstate_t *)0)->client ) },
{ "int", "lastreachabilityarea", (intptr_t)(&((bot_goalstate_t *)0)->lastreachabilityarea), sizeof( ((bot_goalstate_t *)0)->lastreachabilityarea ) },
{ "bot_goal_t[8]", "goalstack", (intptr_t)(&((bot_goalstate_t *)0)->goalstack), sizeof( ((bot_goalstate_t *)0)->goalstack ) },
{ "bot_goal_t[16]", "goalstack", (intptr_t)(&((bot_goalstate_t *)0)->goalstack), sizeof( ((bot_goalstate_t *)0)->goalstack ) },
{ "int", "goalstacktop", (intptr_t)(&((bot_goalstate_t *)0)->goalstacktop), sizeof( ((bot_goalstate_t *)0)->goalstacktop ) },
{ "int[256]", "avoidgoals", (intptr_t)(&((bot_goalstate_t *)0)->avoidgoals), sizeof( ((bot_goalstate_t *)0)->avoidgoals ) },
{ "float[256]", "avoidgoaltimes", (intptr_t)(&((bot_goalstate_t *)0)->avoidgoaltimes), sizeof( ((bot_goalstate_t *)0)->avoidgoaltimes ) },
@@ -8435,7 +8435,7 @@ static classVariableInfo_t idBotGoalManager_typeInfo[] = {
{ ": bot_goalstate_t[33]", "botgoalstates", (intptr_t)(&((idBotGoalManager *)0)->botgoalstates), sizeof( ((idBotGoalManager *)0)->botgoalstates ) },
{ "itemconfig_t", "itemconfiglocal", (intptr_t)(&((idBotGoalManager *)0)->itemconfiglocal), sizeof( ((idBotGoalManager *)0)->itemconfiglocal ) },
{ "itemconfig_t *", "itemconfig", (intptr_t)(&((idBotGoalManager *)0)->itemconfig), sizeof( ((idBotGoalManager *)0)->itemconfig ) },
{ "levelitem_t[256]", "levelitemheap", (intptr_t)(&((idBotGoalManager *)0)->levelitemheap), sizeof( ((idBotGoalManager *)0)->levelitemheap ) },
{ "levelitem_t[2048]", "levelitemheap", (intptr_t)(&((idBotGoalManager *)0)->levelitemheap), sizeof( ((idBotGoalManager *)0)->levelitemheap ) },
{ "levelitem_t *", "freelevelitems", (intptr_t)(&((idBotGoalManager *)0)->freelevelitems), sizeof( ((idBotGoalManager *)0)->freelevelitems ) },
{ "levelitem_t *", "levelitems", (intptr_t)(&((idBotGoalManager *)0)->levelitems), sizeof( ((idBotGoalManager *)0)->levelitems ) },
{ "int", "numlevelitems", (intptr_t)(&((idBotGoalManager *)0)->numlevelitems), sizeof( ((idBotGoalManager *)0)->numlevelitems ) },
@@ -2705,6 +2705,10 @@ void idAsyncServer::RunFrame( void ) {
continue;
}
if (client.channel.GetRemoteAddress().type == NA_BOT) {
continue;
}
// send additional message fragments if the last message was too large to send at once
if ( client.channel.UnsentFragmentsLeft() ) {
client.channel.SendNextFragment( serverPort, serverTime );