Rework bot spawning

- Add a sv_sharedbots to specify if real client slots should be used
- Removed m_bSpawnBot from level, bots are spawned directly in G_BotBegin instead
- sv_numbots can be used to directly configure the number of bots to spawn
- Fix the way bots spawn/unspawn on-demand
- Documented g_bot functions
This commit is contained in:
smallmodel 2024-10-06 22:57:39 +02:00
parent 295c267c31
commit 0d53adf9bd
No known key found for this signature in database
GPG key ID: 9F2D623CEDF08512
8 changed files with 499 additions and 183 deletions

View file

@ -1,6 +1,6 @@
/*
===========================================================================
Copyright (C) 2023 the OpenMoHAA team
Copyright (C) 2024 the OpenMoHAA team
This file is part of OpenMoHAA source code.
@ -26,25 +26,47 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#include "playerbot.h"
#include "g_bot.h"
static gentity_t *firstBot = NULL;
static saved_bot_t *saved_bots = NULL;
static unsigned int num_saved_bots = 0;
static unsigned int current_bot_count = 0;
static unsigned int botId = 0;
static char **modelList = NULL;
Container<str> alliedModelList;
Container<str> germanModelList;
/*
===========
IsAlliedPlayerModel
Return whether or not the specified filename is for allies
============
*/
bool IsAlliedPlayerModel(const char *filename)
{
return !Q_stricmpn(filename, "/allied_", 8) || !Q_stricmpn(filename, "/american_", 10);
}
/*
===========
IsGermanPlayerModel
Return whether or not the specified filename is for axis
============
*/
bool IsGermanPlayerModel(const char *filename)
{
return !Q_stricmpn(filename, "/german_", 8) || !Q_stricmpn(filename, "/IT_", 4) || !Q_stricmpn(filename, "/SC_", 4);
}
/*
===========
IsPlayerModel
Return whether or not the specified filename
is a player model that can be chosen
============
*/
bool IsPlayerModel(const char *filename)
{
size_t len = strlen(filename);
@ -60,12 +82,27 @@ bool IsPlayerModel(const char *filename)
return true;
}
/*
===========
ClearModelList
Clear the allied and axis model list
============
*/
void ClearModelList()
{
alliedModelList.FreeObjectList();
germanModelList.FreeObjectList();
}
/*
===========
InitModelList
Initialize the list of allied and axis player models
that bots can use
============
*/
void InitModelList()
{
char **fileList;
@ -113,12 +150,28 @@ void InitModelList()
gi.FS_FreeFileList(fileList);
}
/*
===========
G_BotBegin
Begin spawning a new bot entity
============
*/
void G_BotBegin(gentity_t *ent)
{
level.m_bSpawnBot = true;
level.spawn_entnum = ent->s.number;
new PlayerBot;
G_ClientBegin(ent, NULL);
}
/*
===========
G_BotThink
Called each server frame to make bots think
============
*/
void G_BotThink(gentity_t *ent, int msec)
{
usercmd_t ucmd;
@ -138,47 +191,222 @@ void G_BotThink(gentity_t *ent, int msec)
G_ClientThink(ent, &ucmd, &eyeinfo);
}
gentity_t *G_GetFirstBot()
/*
===========
G_FindFreeEntityForBot
Find a free client slot
============
*/
gentity_t *G_FindFreeEntityForBot()
{
return firstBot;
gentity_t *ent;
int minNum = 0;
int i;
if (sv_sharedbots->integer) {
minNum = 0;
} else {
minNum = maxclients->integer;
}
void G_AddBot(unsigned int num, saved_bot_t *saved)
for (i = minNum; i < game.maxclients; i++) {
ent = &g_entities[i];
if (!ent->inuse && ent->client && !ent->client->pers.userinfo[0]) {
return ent;
}
}
return NULL;
}
/*
===========
G_ChangeParent
Fix parenting for entities that use the old number
============
*/
void G_ChangeParent(int oldNum, int newNum)
{
int n;
gentity_t *ent;
int i;
int clientNum = -1;
for (i = 0; i < game.maxentities; i++) {
ent = &g_entities[i];
if (!ent->inuse || !ent->entity) {
continue;
}
if (ent->s.parent == oldNum) {
ent->s.parent = newNum;
}
if (ent->r.ownerNum == oldNum) {
ent->r.ownerNum = newNum;
}
}
}
/*
===========
G_BotShift
If the specified slot is used, the bot will be relocated
to the next free entity slot
============
*/
void G_BotShift(int clientNum)
{
gentity_t *ent;
gentity_t *newEnt;
ent = &g_entities[clientNum];
if (!ent->inuse || !ent->client || !ent->entity) {
return;
}
if (!ent->entity->IsSubclassOfBot()) {
return;
}
newEnt = G_FindFreeEntityForBot();
if (!newEnt) {
G_RemoveBot(ent);
return;
}
//
// Allocate the new entity
//
level.spawn_entnum = newEnt - g_entities;
level.AllocEdict(ent->entity);
//
// Copy all fields
//
newEnt->s = ent->s;
newEnt->s.number = newEnt - g_entities;
memcpy(newEnt->client, ent->client, sizeof(*newEnt->client));
newEnt->r = ent->r;
newEnt->solid = ent->solid;
newEnt->tiki = ent->tiki;
AxisCopy(ent->mat, newEnt->mat);
newEnt->freetime = ent->freetime;
newEnt->spawntime = ent->spawntime;
newEnt->radius2 = ent->radius2;
memcpy(newEnt->entname, ent->entname, sizeof(newEnt->entname));
newEnt->clipmask = ent->clipmask;
newEnt->entity = ent->entity;
newEnt->entity->edict = newEnt;
newEnt->entity->client = newEnt->client;
newEnt->entity->entnum = newEnt->s.number;
newEnt->client->ps.clientNum = newEnt->s.number;
G_ChangeParent(ent->s.number, newEnt->s.number);
//
// Free the old entity so the real client will use it
//
level.FreeEdict(ent);
memset(ent->client, 0, sizeof(*ent->client));
G_SetClientConfigString(newEnt);
}
/*
===========
G_GetFirstBot
Return the first bot
============
*/
gentity_t *G_GetFirstBot()
{
gentity_t *ent;
unsigned int n;
for (n = 0; n < game.maxclients; n++) {
ent = &g_entities[n];
if (G_IsBot(ent)) {
return ent;
}
}
return NULL;
}
/*
===========
G_IsBot
Return whether or not the gentity is a bot
============
*/
bool G_IsBot(gentity_t *ent)
{
if (!ent->inuse || !ent->client) {
return false;
}
if (!ent->entity || !ent->entity->IsSubclassOfBot()) {
return false;
}
return true;
}
/*
===========
G_IsPlayer
Return whether or not the gentity is a player
============
*/
bool G_IsPlayer(gentity_t *ent)
{
if (!ent->inuse || !ent->client) {
return false;
}
if (!ent->entity || ent->entity->IsSubclassOfBot()) {
return false;
}
return true;
}
/*
===========
G_AddBot
Add the specified bot, optionally its saved state
============
*/
void G_AddBot(saved_bot_t *saved)
{
int i;
int clientNum;
gentity_t *e;
char botName[MAX_NETNAME];
char challenge[MAX_STRING_TOKENS];
Event *teamEv;
num = Q_min(num, sv_maxbots->integer);
for (n = 0; n < num; n++) {
char userinfo[MAX_INFO_STRING] {0};
for (i = maxclients->integer; i < game.maxclients; i++) {
e = &g_entities[i];
if (!e->inuse && e->client) {
clientNum = i;
break;
}
}
if (clientNum == -1) {
e = G_FindFreeEntityForBot();
if (!e) {
gi.Printf("No free slot for a bot\n");
return;
}
clientNum = e - g_entities;
if (gi.Argc() > 2) {
Q_strncpyz(botName, gi.Argv(2), sizeof(botName));
} else {
Com_sprintf(botName, sizeof(botName), "bot%d", clientNum - maxclients->integer + 1);
Com_sprintf(botName, sizeof(botName), "bot%d", botId);
}
Com_sprintf(challenge, sizeof(challenge), "%d", clientNum - maxclients->integer + 1);
e->s.clientNum = clientNum;
e->s.number = clientNum;
@ -191,25 +419,20 @@ void G_AddBot(unsigned int num, saved_bot_t *saved)
// Choose a random model
//
if (alliedModelList.NumObjects()) {
Info_SetValueForKey(userinfo, "dm_playermodel", alliedModelList[rand() % alliedModelList.NumObjects()]);
const unsigned int index = rand() % alliedModelList.NumObjects();
Info_SetValueForKey(userinfo, "dm_playermodel", alliedModelList[index]);
}
if (germanModelList.NumObjects()) {
Info_SetValueForKey(
userinfo, "dm_playergermanmodel", germanModelList[rand() % germanModelList.NumObjects()]
);
const unsigned int index = rand() % germanModelList.NumObjects();
Info_SetValueForKey(userinfo, "dm_playergermanmodel", germanModelList[index]);
}
Info_SetValueForKey(userinfo, "fov", "80");
Info_SetValueForKey(userinfo, "protocol", "8");
Info_SetValueForKey(userinfo, "ip", "0.0.0.0");
Info_SetValueForKey(userinfo, "qport", "0");
Info_SetValueForKey(userinfo, "challenge", challenge);
Info_SetValueForKey(userinfo, "snaps", "1");
Info_SetValueForKey(userinfo, "rate", "1");
Info_SetValueForKey(userinfo, "dmprimary", "smg");
Info_SetValueForKey(userinfo, "ip", "localhost");
}
current_bot_count++;
botId++;
G_BotConnect(clientNum, userinfo);
@ -217,64 +440,71 @@ void G_AddBot(unsigned int num, saved_bot_t *saved)
e->client->pers = saved->pers;
}
if (!firstBot) {
firstBot = e;
}
G_BotBegin(e);
}
if (saved) {
/*
switch (saved->team)
{
case TEAM_ALLIES:
teamEv = new Event(EV_Player_JoinDMTeam);
teamEv->AddString("allies");
break;
case TEAM_AXIS:
teamEv = new Event(EV_Player_JoinDMTeam);
teamEv->AddString("axis");
break;
default:
teamEv = new Event(EV_Player_AutoJoinDMTeam);
break;
}
===========
G_AddBots
Add the specified number of bots
============
*/
} else {
teamEv = new Event(EV_Player_AutoJoinDMTeam);
e->entity->PostEvent(teamEv, level.frametime);
void G_AddBots(unsigned int num)
{
int n;
Event *ev = new Event(EV_Player_PrimaryDMWeapon);
ev->AddString("auto");
e->entity->PostEvent(ev, level.frametime);
}
for (n = 0; n < num; n++) {
G_AddBot(NULL);
}
}
void G_RemoveBot(unsigned int num)
/*
===========
G_RemoveBot
Remove the specified bot
============
*/
void G_RemoveBot(gentity_t *ent)
{
G_ClientDisconnect(ent);
current_bot_count--;
}
/*
===========
G_RemoveBots
Remove the specified number of bots
============
*/
void G_RemoveBots(unsigned int num)
{
unsigned int removed = 0;
unsigned int n;
unsigned int teamCount[2] {0};
bool bNoMoreToRemove = false;
num = Q_min(num, sv_maxbots->integer);
teamCount[0] = dmManager.GetTeamAllies()->m_players.NumObjects();
teamCount[1] = dmManager.GetTeamAxis()->m_players.NumObjects();
while (!bNoMoreToRemove) {
bNoMoreToRemove = true;
// First remove bots that are in the team
// with the higest player count
for (n = 0; n < game.maxclients && removed < num; n++) {
gentity_t *e = &g_entities[game.maxclients - sv_maxbots->integer + n];
if (e->inuse && e->client) {
gentity_t *e = &g_entities[n];
if (!G_IsBot(e)) {
continue;
}
Player *player = static_cast<Player *>(e->entity);
if (player->GetTeam() == TEAM_ALLIES || player->GetTeam() == TEAM_AXIS) {
unsigned int teamIndex = (player->GetTeam() - TEAM_ALLIES);
if (teamCount[teamIndex] < teamCount[1 - teamIndex]) {
// Skip bots in the lowest team
// Not enough players in that team, don't remove the bot
continue;
}
@ -282,26 +512,32 @@ void G_RemoveBot(unsigned int num)
bNoMoreToRemove = false;
}
G_ClientDisconnect(e);
current_bot_count--;
G_RemoveBot(e);
removed++;
}
}
}
//
// Remove all bots regardless
// Remove all bots that haven't been removed earlier
//
for (n = 0; n < game.maxclients && removed < num; n++) {
gentity_t *e = &g_entities[game.maxclients - sv_maxbots->integer + n];
if (e->inuse && e->client) {
G_ClientDisconnect(e);
current_bot_count--;
gentity_t *e = &g_entities[n];
if (!G_IsBot(e)) {
continue;
}
G_RemoveBot(e);
removed++;
}
}
}
/*
===========
G_SaveBots
Save bot persistent data
============
*/
void G_SaveBots()
{
unsigned int n;
@ -318,9 +554,11 @@ void G_SaveBots()
saved_bots = new saved_bot_t[current_bot_count];
num_saved_bots = 0;
for (n = 0; n < game.maxclients; n++) {
gentity_t *e = &g_entities[game.maxclients - sv_maxbots->integer + n];
gentity_t *e = &g_entities[n];
if (!G_IsBot(e)) {
continue;
}
if (e->inuse && e->client) {
Player *player = static_cast<Player *>(e->entity);
saved_bot_t& saved = saved_bots[num_saved_bots++];
@ -329,8 +567,14 @@ void G_SaveBots()
saved.pers = player->client->pers;
}
}
}
/*
===========
G_RestoreBots
Restore bot persistent data, such as their team
============
*/
void G_RestoreBots()
{
unsigned int n;
@ -342,28 +586,60 @@ void G_RestoreBots()
for (n = 0; n < num_saved_bots; n++) {
saved_bot_t& saved = saved_bots[n];
G_AddBot(1, &saved);
G_AddBot(&saved);
}
delete[] saved_bots;
saved_bots = NULL;
}
/*
===========
G_CountPlayingClients
Count the number of real clients that are playing
============
*/
int G_CountPlayingClients()
{
gentity_t *other;
unsigned int n;
unsigned int count = 0;
for (n = 0; n < game.maxclients; n++) {
other = &g_entities[n];
if (G_IsPlayer(other)) {
Player *p = static_cast<Player *>(other->entity);
// Ignore spectators
if (p->GetTeam() != teamtype_t::TEAM_NONE && p->GetTeam() != teamtype_t::TEAM_SPECTATOR) {
count++;
}
}
}
return count;
}
/*
===========
G_CountClients
Count the number of real clients
============
*/
int G_CountClients()
{
gentity_t *other;
unsigned int n;
unsigned int count = 0;
for (n = 0; n < maxclients->integer; n++) {
for (n = 0; n < game.maxclients; n++) {
other = &g_entities[n];
if (other->inuse && other->client) {
Player *p = static_cast<Player *>(other->entity);
if (p->GetTeam() == teamtype_t::TEAM_NONE || p->GetTeam() == teamtype_t::TEAM_SPECTATOR) {
// ignore spectators
if (G_IsBot(other)) {
continue;
}
if (other->client && other->client->pers.userinfo[0]) {
count++;
}
}
@ -371,13 +647,28 @@ int G_CountClients()
return count;
}
/*
===========
G_ResetBots
Save and reset the bot count
============
*/
void G_ResetBots()
{
G_SaveBots();
current_bot_count = 0;
botId = 0;
}
/*
===========
G_SpawnBots
Called each frame to manage bot spawning
============
*/
void G_SpawnBots()
{
unsigned int numClients;
@ -392,19 +683,30 @@ void G_SpawnBots()
//
// Check the minimum bot count
//
numClients = G_CountClients();
numClients = G_CountPlayingClients();
if (numClients < sv_minPlayers->integer) {
numBotsToSpawn = sv_minPlayers->integer - numClients + sv_numbots->integer;
} else {
numBotsToSpawn = sv_numbots->integer;
}
if (sv_sharedbots->integer) {
unsigned int numClients = G_CountClients();
//
// Cap to the maximum number of possible clients
//
numBotsToSpawn = Q_min(numBotsToSpawn, maxclients->integer - numClients + sv_maxbots->integer);
} else {
numBotsToSpawn = Q_min(numBotsToSpawn, sv_maxbots->integer);
}
//
// Spawn bots
//
if (numBotsToSpawn > current_bot_count) {
G_AddBot(numBotsToSpawn - current_bot_count);
G_AddBots(numBotsToSpawn - current_bot_count);
} else if (numBotsToSpawn < current_bot_count) {
G_RemoveBot(current_bot_count - numBotsToSpawn);
G_RemoveBots(current_bot_count - numBotsToSpawn);
}
}

View file

@ -1,6 +1,6 @@
/*
===========================================================================
Copyright (C) 2023 the OpenMoHAA team
Copyright (C) 2024 the OpenMoHAA team
This file is part of OpenMoHAA source code.
@ -33,8 +33,13 @@ struct saved_bot_t {
void G_BotBegin(gentity_t *ent);
void G_BotThink(gentity_t *ent, int msec);
void G_BotShift(int clientNum);
gentity_t *G_GetFirstBot();
void G_AddBot(unsigned int num, saved_bot_t* saved = NULL);
void G_RemoveBot(unsigned int num);
void G_AddBot(saved_bot_t *saved = NULL);
void G_AddBots(unsigned int num);
void G_RemoveBot(gentity_t *ent);
void G_RemoveBots(unsigned int num);
bool G_IsBot(gentity_t *ent);
bool G_IsPlayer(gentity_t *ent);
void G_ResetBots();
void G_SpawnBots();

View file

@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#include "playerstart.h"
#include "scriptmaster.h"
#include "g_spawn.h"
#include "g_bot.h"
// g_client.c -- client functions that don't happen every frame
@ -833,6 +834,9 @@ void G_BotConnect(int clientNum, const char *userinfo)
memset(client, 0, sizeof(*client));
G_InitClientPersistant(client, userinfo);
//
// Use "localhost" as some code relies on it to check whether or not it should be kicked
//
Q_strncpyz(client->pers.ip, "localhost", sizeof(client->pers.ip));
client->pers.port = 0;
@ -873,6 +877,9 @@ const char *G_ClientConnect(int clientNum, qboolean firstTime, qboolean differen
// return NULL;
//}
// Added in OPM
G_BotShift(clientNum);
ent = &g_entities[clientNum];
gi.GetUserinfo(clientNum, userinfo, sizeof(userinfo));
@ -969,14 +976,8 @@ void G_ClientBegin(gentity_t *ent, usercmd_t *cmd)
} else {
// a spawn point will completely reinitialize the entity
level.spawn_entnum = ent->s.number;
if (level.m_bSpawnBot) {
level.m_bSpawnBot = false;
PlayerBot *player = new PlayerBot;
} else {
Player *player = new Player;
}
}
if (level.intermissiontime && ent->entity) {
G_MoveClientToIntermission(ent->entity);

View file

@ -265,9 +265,20 @@ cvar_t *g_obituarylocation;
cvar_t *sv_scriptfiles;
// The maximum number of allocated bot clients
cvar_t *sv_maxbots;
// The number of bots that should be spawned
cvar_t *sv_numbots;
// The minimum number of players that should be present in-game.
// If the number of real players is below this number,
// the game will automatically add bots to fill the gap
cvar_t *sv_minPlayers;
// Whether or not the bots use a shared player slots
// NOTE: Setting this cvar is not recommended
// because when a client connects and the slot is used by a bot
// the bot will be relocated to a free entity slot
cvar_t *sv_sharedbots;
cvar_t *g_rankedserver;
cvar_t *g_spectatefollow_firstperson;
@ -632,7 +643,8 @@ void CVAR_Init(void)
sv_scriptfiles = gi.Cvar_Get("sv_scriptfiles", "0", 0);
sv_maxbots = gi.Cvar_Get("sv_maxbots", "0", CVAR_LATCH);
sv_numbots = gi.Cvar_Get("sv_numbots", "0", CVAR_LATCH);
sv_sharedbots = gi.Cvar_Get("sv_sharedbots", "0", CVAR_LATCH);
sv_numbots = gi.Cvar_Get("sv_numbots", "0", 0);
sv_minPlayers = gi.Cvar_Get("sv_minPlayers", "0", 0);
g_rankedserver = gi.Cvar_Get("g_rankedserver", "0", 0);
g_spectatefollow_firstperson = gi.Cvar_Get("g_spectatefollow_firstperson", "0", 0);
@ -646,11 +658,6 @@ void CVAR_Init(void)
gi.Printf("sv_maxbots reached max clients, lowering the value to %u\n", lowered);
}
if (sv_numbots->integer > sv_maxbots->integer) {
gi.Printf("numbots overflow, setting to %d\n", sv_maxbots->integer);
gi.cvar_set("sv_numbots", sv_maxbots->string);
}
g_instamsg_allowed = gi.Cvar_Get("g_instamsg_allowed", "1", 0);
g_instamsg_minDelay = gi.Cvar_Get("g_instamsg_minDelay", "1000", 0);
g_textmsg_allowed = gi.Cvar_Get("g_textmsg_allowed", "1", 0);

View file

@ -269,6 +269,7 @@ extern cvar_t *sv_scriptfiles;
extern cvar_t *sv_maxbots;
extern cvar_t *sv_numbots;
extern cvar_t *sv_minPlayers;
extern cvar_t *sv_sharedbots;
extern cvar_t *g_rankedserver;
extern cvar_t *g_spectatefollow_firstperson;

View file

@ -834,7 +834,6 @@ void Level::Init(void)
m_pAIStats = NULL;
m_bSpawnBot = false;
m_bScriptSpawn = false;
m_bRejectSpawn = false;
}

View file

@ -251,8 +251,6 @@ public:
// New Stuff
bool m_bSpawnBot;
// Script stuff
bool m_bScriptSpawn;
bool m_bRejectSpawn;

View file

@ -95,7 +95,10 @@ This feature is passive: it only checks the team sizes when someone tries to joi
### Bots
Bots can be used for testing. They don't move by default, so a mod will be needed, like [eaglear bots](https://www.moddb.com/mods/medal-of-honor-world-war-1/downloads/moh-eaglear-bots):
- `set sv_maxbots x`: Configure and allocate the maximum number of bots.
- `set sv_minPlayers x`: optional, can be used to set the minimum number of players that the server should have. Bots will be spawned based on the minimum number of players.
- `addbot x`: x is the number of bots to add.
- `removebot x`: x is the number of bots to remove.
- `set sv_maxbots x`: Required, configure the maximum number of bots allowed in the game. Since the game can only handle a total of 64 players (clients), the number of bots will be limited to 64 minus the number of real players (`sv_maxclients`). For example, if you set `sv_maxclients` to 48, the maximum number of bots (sv_maxbots) can be 16.
- `set sv_numbots x`: Set the number of bots to spawn. It will be capped to the value of `sv_maxbots`.
- `set sv_minPlayers x`: Configure the minimum number of players required. If the number of real players is below the specified value, the game will automatically add bots to fill the gap. For example, if `sv_minPlayers` is set to 8 and only 5 real players are connected, the game will spawn 3 bots to make sure there are always 8 players in the game.
Commands:
- `addbot x`: x is the number of bots to add. It only changes the `sv_numbots` variable.
- `removebot x`: x is the number of bots to remove. It only changes the `sv_numbots` variable.