mirror of
https://github.com/openmoh/openmohaa.git
synced 2025-04-28 13:47:58 +03:00
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:
parent
295c267c31
commit
0d53adf9bd
8 changed files with 499 additions and 183 deletions
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
===========================================================================
|
===========================================================================
|
||||||
Copyright (C) 2023 the OpenMoHAA team
|
Copyright (C) 2024 the OpenMoHAA team
|
||||||
|
|
||||||
This file is part of OpenMoHAA source code.
|
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 "playerbot.h"
|
||||||
#include "g_bot.h"
|
#include "g_bot.h"
|
||||||
|
|
||||||
static gentity_t *firstBot = NULL;
|
|
||||||
static saved_bot_t *saved_bots = NULL;
|
static saved_bot_t *saved_bots = NULL;
|
||||||
static unsigned int num_saved_bots = 0;
|
static unsigned int num_saved_bots = 0;
|
||||||
static unsigned int current_bot_count = 0;
|
static unsigned int current_bot_count = 0;
|
||||||
|
static unsigned int botId = 0;
|
||||||
static char **modelList = NULL;
|
static char **modelList = NULL;
|
||||||
|
|
||||||
Container<str> alliedModelList;
|
Container<str> alliedModelList;
|
||||||
Container<str> germanModelList;
|
Container<str> germanModelList;
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
IsAlliedPlayerModel
|
||||||
|
|
||||||
|
Return whether or not the specified filename is for allies
|
||||||
|
============
|
||||||
|
*/
|
||||||
bool IsAlliedPlayerModel(const char *filename)
|
bool IsAlliedPlayerModel(const char *filename)
|
||||||
{
|
{
|
||||||
return !Q_stricmpn(filename, "/allied_", 8) || !Q_stricmpn(filename, "/american_", 10);
|
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)
|
bool IsGermanPlayerModel(const char *filename)
|
||||||
{
|
{
|
||||||
return !Q_stricmpn(filename, "/german_", 8) || !Q_stricmpn(filename, "/IT_", 4) || !Q_stricmpn(filename, "/SC_", 4);
|
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)
|
bool IsPlayerModel(const char *filename)
|
||||||
{
|
{
|
||||||
size_t len = strlen(filename);
|
size_t len = strlen(filename);
|
||||||
|
@ -60,12 +82,27 @@ bool IsPlayerModel(const char *filename)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
ClearModelList
|
||||||
|
|
||||||
|
Clear the allied and axis model list
|
||||||
|
============
|
||||||
|
*/
|
||||||
void ClearModelList()
|
void ClearModelList()
|
||||||
{
|
{
|
||||||
alliedModelList.FreeObjectList();
|
alliedModelList.FreeObjectList();
|
||||||
germanModelList.FreeObjectList();
|
germanModelList.FreeObjectList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
InitModelList
|
||||||
|
|
||||||
|
Initialize the list of allied and axis player models
|
||||||
|
that bots can use
|
||||||
|
============
|
||||||
|
*/
|
||||||
void InitModelList()
|
void InitModelList()
|
||||||
{
|
{
|
||||||
char **fileList;
|
char **fileList;
|
||||||
|
@ -113,12 +150,28 @@ void InitModelList()
|
||||||
gi.FS_FreeFileList(fileList);
|
gi.FS_FreeFileList(fileList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_BotBegin
|
||||||
|
|
||||||
|
Begin spawning a new bot entity
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_BotBegin(gentity_t *ent)
|
void G_BotBegin(gentity_t *ent)
|
||||||
{
|
{
|
||||||
level.m_bSpawnBot = true;
|
level.spawn_entnum = ent->s.number;
|
||||||
|
new PlayerBot;
|
||||||
|
|
||||||
G_ClientBegin(ent, NULL);
|
G_ClientBegin(ent, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_BotThink
|
||||||
|
|
||||||
|
Called each server frame to make bots think
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_BotThink(gentity_t *ent, int msec)
|
void G_BotThink(gentity_t *ent, int msec)
|
||||||
{
|
{
|
||||||
usercmd_t ucmd;
|
usercmd_t ucmd;
|
||||||
|
@ -138,128 +191,300 @@ void G_BotThink(gentity_t *ent, int msec)
|
||||||
G_ClientThink(ent, &ucmd, &eyeinfo);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void G_AddBot(unsigned int num, saved_bot_t *saved)
|
/*
|
||||||
|
===========
|
||||||
|
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 i;
|
||||||
int clientNum = -1;
|
|
||||||
gentity_t *e;
|
|
||||||
char botName[MAX_NETNAME];
|
|
||||||
char challenge[MAX_STRING_TOKENS];
|
|
||||||
Event *teamEv;
|
|
||||||
|
|
||||||
num = Q_min(num, sv_maxbots->integer);
|
for (i = 0; i < game.maxentities; i++) {
|
||||||
for (n = 0; n < num; n++) {
|
ent = &g_entities[i];
|
||||||
char userinfo[MAX_INFO_STRING] {0};
|
if (!ent->inuse || !ent->entity) {
|
||||||
|
continue;
|
||||||
for (i = maxclients->integer; i < game.maxclients; i++) {
|
|
||||||
e = &g_entities[i];
|
|
||||||
|
|
||||||
if (!e->inuse && e->client) {
|
|
||||||
clientNum = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientNum == -1) {
|
if (ent->s.parent == oldNum) {
|
||||||
gi.Printf("No free slot for a bot\n");
|
ent->s.parent = newNum;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (ent->r.ownerNum == oldNum) {
|
||||||
if (gi.Argc() > 2) {
|
ent->r.ownerNum = newNum;
|
||||||
Q_strncpyz(botName, gi.Argv(2), sizeof(botName));
|
|
||||||
} else {
|
|
||||||
Com_sprintf(botName, sizeof(botName), "bot%d", clientNum - maxclients->integer + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Com_sprintf(challenge, sizeof(challenge), "%d", clientNum - maxclients->integer + 1);
|
|
||||||
|
|
||||||
e->s.clientNum = clientNum;
|
|
||||||
e->s.number = clientNum;
|
|
||||||
|
|
||||||
if (saved) {
|
|
||||||
strncpy(userinfo, saved->pers.userinfo, ARRAY_LEN(userinfo));
|
|
||||||
} else {
|
|
||||||
Info_SetValueForKey(userinfo, "name", botName);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Choose a random model
|
|
||||||
//
|
|
||||||
if (alliedModelList.NumObjects()) {
|
|
||||||
Info_SetValueForKey(userinfo, "dm_playermodel", alliedModelList[rand() % alliedModelList.NumObjects()]);
|
|
||||||
}
|
|
||||||
if (germanModelList.NumObjects()) {
|
|
||||||
Info_SetValueForKey(
|
|
||||||
userinfo, "dm_playergermanmodel", germanModelList[rand() % germanModelList.NumObjects()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
current_bot_count++;
|
|
||||||
|
|
||||||
G_BotConnect(clientNum, userinfo);
|
|
||||||
|
|
||||||
if (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;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} else {
|
|
||||||
teamEv = new Event(EV_Player_AutoJoinDMTeam);
|
|
||||||
e->entity->PostEvent(teamEv, level.frametime);
|
|
||||||
|
|
||||||
Event *ev = new Event(EV_Player_PrimaryDMWeapon);
|
|
||||||
ev->AddString("auto");
|
|
||||||
|
|
||||||
e->entity->PostEvent(ev, level.frametime);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void G_RemoveBot(unsigned int num)
|
/*
|
||||||
|
===========
|
||||||
|
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;
|
||||||
|
char userinfo[MAX_INFO_STRING] {0};
|
||||||
|
|
||||||
|
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", botId);
|
||||||
|
}
|
||||||
|
|
||||||
|
e->s.clientNum = clientNum;
|
||||||
|
e->s.number = clientNum;
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
strncpy(userinfo, saved->pers.userinfo, ARRAY_LEN(userinfo));
|
||||||
|
} else {
|
||||||
|
Info_SetValueForKey(userinfo, "name", botName);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Choose a random model
|
||||||
|
//
|
||||||
|
if (alliedModelList.NumObjects()) {
|
||||||
|
const unsigned int index = rand() % alliedModelList.NumObjects();
|
||||||
|
Info_SetValueForKey(userinfo, "dm_playermodel", alliedModelList[index]);
|
||||||
|
}
|
||||||
|
if (germanModelList.NumObjects()) {
|
||||||
|
const unsigned int index = rand() % germanModelList.NumObjects();
|
||||||
|
Info_SetValueForKey(userinfo, "dm_playergermanmodel", germanModelList[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Info_SetValueForKey(userinfo, "fov", "80");
|
||||||
|
Info_SetValueForKey(userinfo, "ip", "localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
current_bot_count++;
|
||||||
|
botId++;
|
||||||
|
|
||||||
|
G_BotConnect(clientNum, userinfo);
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
e->client->pers = saved->pers;
|
||||||
|
}
|
||||||
|
|
||||||
|
G_BotBegin(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_AddBots
|
||||||
|
|
||||||
|
Add the specified number of bots
|
||||||
|
============
|
||||||
|
*/
|
||||||
|
void G_AddBots(unsigned int num)
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
|
||||||
|
for (n = 0; n < num; n++) {
|
||||||
|
G_AddBot(NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
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 removed = 0;
|
||||||
unsigned int n;
|
unsigned int n;
|
||||||
unsigned int teamCount[2]{ 0 };
|
unsigned int teamCount[2] {0};
|
||||||
bool bNoMoreToRemove = false;
|
bool bNoMoreToRemove = false;
|
||||||
|
|
||||||
num = Q_min(num, sv_maxbots->integer);
|
|
||||||
|
|
||||||
teamCount[0] = dmManager.GetTeamAllies()->m_players.NumObjects();
|
teamCount[0] = dmManager.GetTeamAllies()->m_players.NumObjects();
|
||||||
teamCount[1] = dmManager.GetTeamAxis()->m_players.NumObjects();
|
teamCount[1] = dmManager.GetTeamAxis()->m_players.NumObjects();
|
||||||
|
@ -267,41 +492,52 @@ void G_RemoveBot(unsigned int num)
|
||||||
while (!bNoMoreToRemove) {
|
while (!bNoMoreToRemove) {
|
||||||
bNoMoreToRemove = true;
|
bNoMoreToRemove = true;
|
||||||
|
|
||||||
|
// First remove bots that are in the team
|
||||||
|
// with the higest player count
|
||||||
for (n = 0; n < game.maxclients && removed < num; n++) {
|
for (n = 0; n < game.maxclients && removed < num; n++) {
|
||||||
gentity_t *e = &g_entities[game.maxclients - sv_maxbots->integer + n];
|
gentity_t *e = &g_entities[n];
|
||||||
if (e->inuse && e->client) {
|
if (!G_IsBot(e)) {
|
||||||
Player* player = static_cast<Player*>(e->entity);
|
continue;
|
||||||
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
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
teamCount[teamIndex]--;
|
Player *player = static_cast<Player *>(e->entity);
|
||||||
bNoMoreToRemove = false;
|
if (player->GetTeam() == TEAM_ALLIES || player->GetTeam() == TEAM_AXIS) {
|
||||||
|
unsigned int teamIndex = (player->GetTeam() - TEAM_ALLIES);
|
||||||
|
if (teamCount[teamIndex] < teamCount[1 - teamIndex]) {
|
||||||
|
// Not enough players in that team, don't remove the bot
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
G_ClientDisconnect(e);
|
teamCount[teamIndex]--;
|
||||||
current_bot_count--;
|
bNoMoreToRemove = false;
|
||||||
removed++;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
G_RemoveBot(e);
|
||||||
// Remove all bots regardless
|
|
||||||
//
|
|
||||||
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--;
|
|
||||||
removed++;
|
removed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remove all bots that haven't been removed earlier
|
||||||
|
//
|
||||||
|
for (n = 0; n < game.maxclients && removed < num; n++) {
|
||||||
|
gentity_t *e = &g_entities[n];
|
||||||
|
if (!G_IsBot(e)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
G_RemoveBot(e);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_SaveBots
|
||||||
|
|
||||||
|
Save bot persistent data
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_SaveBots()
|
void G_SaveBots()
|
||||||
{
|
{
|
||||||
unsigned int n;
|
unsigned int n;
|
||||||
|
@ -315,22 +551,30 @@ void G_SaveBots()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saved_bots = new saved_bot_t[current_bot_count];
|
saved_bots = new saved_bot_t[current_bot_count];
|
||||||
num_saved_bots = 0;
|
num_saved_bots = 0;
|
||||||
for (n = 0; n < game.maxclients; n++) {
|
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)) {
|
||||||
if (e->inuse && e->client) {
|
continue;
|
||||||
Player *player = static_cast<Player *>(e->entity);
|
|
||||||
saved_bot_t& saved = saved_bots[num_saved_bots++];
|
|
||||||
|
|
||||||
saved.bValid = true;
|
|
||||||
//saved.team = player->GetTeam();
|
|
||||||
saved.pers = player->client->pers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Player *player = static_cast<Player *>(e->entity);
|
||||||
|
saved_bot_t& saved = saved_bots[num_saved_bots++];
|
||||||
|
|
||||||
|
saved.bValid = true;
|
||||||
|
//saved.team = player->GetTeam();
|
||||||
|
saved.pers = player->client->pers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_RestoreBots
|
||||||
|
|
||||||
|
Restore bot persistent data, such as their team
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_RestoreBots()
|
void G_RestoreBots()
|
||||||
{
|
{
|
||||||
unsigned int n;
|
unsigned int n;
|
||||||
|
@ -342,28 +586,60 @@ void G_RestoreBots()
|
||||||
for (n = 0; n < num_saved_bots; n++) {
|
for (n = 0; n < num_saved_bots; n++) {
|
||||||
saved_bot_t& saved = saved_bots[n];
|
saved_bot_t& saved = saved_bots[n];
|
||||||
|
|
||||||
G_AddBot(1, &saved);
|
G_AddBot(&saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete[] saved_bots;
|
delete[] saved_bots;
|
||||||
saved_bots = NULL;
|
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()
|
int G_CountClients()
|
||||||
{
|
{
|
||||||
gentity_t *other;
|
gentity_t *other;
|
||||||
unsigned int n;
|
unsigned int n;
|
||||||
unsigned int count = 0;
|
unsigned int count = 0;
|
||||||
|
|
||||||
for (n = 0; n < maxclients->integer; n++) {
|
for (n = 0; n < game.maxclients; n++) {
|
||||||
other = &g_entities[n];
|
other = &g_entities[n];
|
||||||
if (other->inuse && other->client) {
|
if (G_IsBot(other)) {
|
||||||
Player *p = static_cast<Player *>(other->entity);
|
continue;
|
||||||
if (p->GetTeam() == teamtype_t::TEAM_NONE || p->GetTeam() == teamtype_t::TEAM_SPECTATOR) {
|
}
|
||||||
// ignore spectators
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (other->client && other->client->pers.userinfo[0]) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,13 +647,28 @@ int G_CountClients()
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_ResetBots
|
||||||
|
|
||||||
|
Save and reset the bot count
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_ResetBots()
|
void G_ResetBots()
|
||||||
{
|
{
|
||||||
G_SaveBots();
|
G_SaveBots();
|
||||||
|
|
||||||
current_bot_count = 0;
|
current_bot_count = 0;
|
||||||
|
botId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
===========
|
||||||
|
G_SpawnBots
|
||||||
|
|
||||||
|
Called each frame to manage bot spawning
|
||||||
|
============
|
||||||
|
*/
|
||||||
void G_SpawnBots()
|
void G_SpawnBots()
|
||||||
{
|
{
|
||||||
unsigned int numClients;
|
unsigned int numClients;
|
||||||
|
@ -392,19 +683,30 @@ void G_SpawnBots()
|
||||||
//
|
//
|
||||||
// Check the minimum bot count
|
// Check the minimum bot count
|
||||||
//
|
//
|
||||||
numClients = G_CountClients();
|
numClients = G_CountPlayingClients();
|
||||||
if (numClients < sv_minPlayers->integer) {
|
if (numClients < sv_minPlayers->integer) {
|
||||||
numBotsToSpawn = sv_minPlayers->integer - numClients + sv_numbots->integer;
|
numBotsToSpawn = sv_minPlayers->integer - numClients + sv_numbots->integer;
|
||||||
} else {
|
} else {
|
||||||
numBotsToSpawn = sv_numbots->integer;
|
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
|
// Spawn bots
|
||||||
//
|
//
|
||||||
if (numBotsToSpawn > current_bot_count) {
|
if (numBotsToSpawn > current_bot_count) {
|
||||||
G_AddBot(numBotsToSpawn - current_bot_count);
|
G_AddBots(numBotsToSpawn - current_bot_count);
|
||||||
} else if (numBotsToSpawn < current_bot_count) {
|
} else if (numBotsToSpawn < current_bot_count) {
|
||||||
G_RemoveBot(current_bot_count - numBotsToSpawn);
|
G_RemoveBots(current_bot_count - numBotsToSpawn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
===========================================================================
|
===========================================================================
|
||||||
Copyright (C) 2023 the OpenMoHAA team
|
Copyright (C) 2024 the OpenMoHAA team
|
||||||
|
|
||||||
This file is part of OpenMoHAA source code.
|
This file is part of OpenMoHAA source code.
|
||||||
|
|
||||||
|
@ -24,17 +24,22 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
struct saved_bot_t {
|
struct saved_bot_t {
|
||||||
client_persistant_t pers;
|
client_persistant_t pers;
|
||||||
bool bValid;
|
bool bValid;
|
||||||
|
|
||||||
saved_bot_t()
|
saved_bot_t()
|
||||||
: bValid(false)
|
: bValid(false)
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
void G_BotBegin(gentity_t* ent);
|
void G_BotBegin(gentity_t *ent);
|
||||||
void G_BotThink(gentity_t* ent, int msec);
|
void G_BotThink(gentity_t *ent, int msec);
|
||||||
gentity_t* G_GetFirstBot();
|
void G_BotShift(int clientNum);
|
||||||
void G_AddBot(unsigned int num, saved_bot_t* saved = NULL);
|
gentity_t *G_GetFirstBot();
|
||||||
void G_RemoveBot(unsigned int num);
|
void G_AddBot(saved_bot_t *saved = NULL);
|
||||||
void G_ResetBots();
|
void G_AddBots(unsigned int num);
|
||||||
void G_SpawnBots();
|
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();
|
|
@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include "playerstart.h"
|
#include "playerstart.h"
|
||||||
#include "scriptmaster.h"
|
#include "scriptmaster.h"
|
||||||
#include "g_spawn.h"
|
#include "g_spawn.h"
|
||||||
|
#include "g_bot.h"
|
||||||
|
|
||||||
// g_client.c -- client functions that don't happen every frame
|
// 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));
|
memset(client, 0, sizeof(*client));
|
||||||
G_InitClientPersistant(client, userinfo);
|
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));
|
Q_strncpyz(client->pers.ip, "localhost", sizeof(client->pers.ip));
|
||||||
client->pers.port = 0;
|
client->pers.port = 0;
|
||||||
|
|
||||||
|
@ -873,6 +877,9 @@ const char *G_ClientConnect(int clientNum, qboolean firstTime, qboolean differen
|
||||||
// return NULL;
|
// return NULL;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
// Added in OPM
|
||||||
|
G_BotShift(clientNum);
|
||||||
|
|
||||||
ent = &g_entities[clientNum];
|
ent = &g_entities[clientNum];
|
||||||
|
|
||||||
gi.GetUserinfo(clientNum, userinfo, sizeof(userinfo));
|
gi.GetUserinfo(clientNum, userinfo, sizeof(userinfo));
|
||||||
|
@ -969,13 +976,7 @@ void G_ClientBegin(gentity_t *ent, usercmd_t *cmd)
|
||||||
} else {
|
} else {
|
||||||
// a spawn point will completely reinitialize the entity
|
// a spawn point will completely reinitialize the entity
|
||||||
level.spawn_entnum = ent->s.number;
|
level.spawn_entnum = ent->s.number;
|
||||||
|
Player *player = new Player;
|
||||||
if (level.m_bSpawnBot) {
|
|
||||||
level.m_bSpawnBot = false;
|
|
||||||
PlayerBot *player = new PlayerBot;
|
|
||||||
} else {
|
|
||||||
Player *player = new Player;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level.intermissiontime && ent->entity) {
|
if (level.intermissiontime && ent->entity) {
|
||||||
|
|
|
@ -265,9 +265,20 @@ cvar_t *g_obituarylocation;
|
||||||
|
|
||||||
cvar_t *sv_scriptfiles;
|
cvar_t *sv_scriptfiles;
|
||||||
|
|
||||||
|
// The maximum number of allocated bot clients
|
||||||
cvar_t *sv_maxbots;
|
cvar_t *sv_maxbots;
|
||||||
|
// The number of bots that should be spawned
|
||||||
cvar_t *sv_numbots;
|
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;
|
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_rankedserver;
|
||||||
cvar_t *g_spectatefollow_firstperson;
|
cvar_t *g_spectatefollow_firstperson;
|
||||||
|
|
||||||
|
@ -632,7 +643,8 @@ void CVAR_Init(void)
|
||||||
|
|
||||||
sv_scriptfiles = gi.Cvar_Get("sv_scriptfiles", "0", 0);
|
sv_scriptfiles = gi.Cvar_Get("sv_scriptfiles", "0", 0);
|
||||||
sv_maxbots = gi.Cvar_Get("sv_maxbots", "0", CVAR_LATCH);
|
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);
|
sv_minPlayers = gi.Cvar_Get("sv_minPlayers", "0", 0);
|
||||||
g_rankedserver = gi.Cvar_Get("g_rankedserver", "0", 0);
|
g_rankedserver = gi.Cvar_Get("g_rankedserver", "0", 0);
|
||||||
g_spectatefollow_firstperson = gi.Cvar_Get("g_spectatefollow_firstperson", "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);
|
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_allowed = gi.Cvar_Get("g_instamsg_allowed", "1", 0);
|
||||||
g_instamsg_minDelay = gi.Cvar_Get("g_instamsg_minDelay", "1000", 0);
|
g_instamsg_minDelay = gi.Cvar_Get("g_instamsg_minDelay", "1000", 0);
|
||||||
g_textmsg_allowed = gi.Cvar_Get("g_textmsg_allowed", "1", 0);
|
g_textmsg_allowed = gi.Cvar_Get("g_textmsg_allowed", "1", 0);
|
||||||
|
|
|
@ -269,6 +269,7 @@ extern cvar_t *sv_scriptfiles;
|
||||||
extern cvar_t *sv_maxbots;
|
extern cvar_t *sv_maxbots;
|
||||||
extern cvar_t *sv_numbots;
|
extern cvar_t *sv_numbots;
|
||||||
extern cvar_t *sv_minPlayers;
|
extern cvar_t *sv_minPlayers;
|
||||||
|
extern cvar_t *sv_sharedbots;
|
||||||
extern cvar_t *g_rankedserver;
|
extern cvar_t *g_rankedserver;
|
||||||
extern cvar_t *g_spectatefollow_firstperson;
|
extern cvar_t *g_spectatefollow_firstperson;
|
||||||
|
|
||||||
|
|
|
@ -834,7 +834,6 @@ void Level::Init(void)
|
||||||
|
|
||||||
m_pAIStats = NULL;
|
m_pAIStats = NULL;
|
||||||
|
|
||||||
m_bSpawnBot = false;
|
|
||||||
m_bScriptSpawn = false;
|
m_bScriptSpawn = false;
|
||||||
m_bRejectSpawn = false;
|
m_bRejectSpawn = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,8 +251,6 @@ public:
|
||||||
|
|
||||||
// New Stuff
|
// New Stuff
|
||||||
|
|
||||||
bool m_bSpawnBot;
|
|
||||||
|
|
||||||
// Script stuff
|
// Script stuff
|
||||||
bool m_bScriptSpawn;
|
bool m_bScriptSpawn;
|
||||||
bool m_bRejectSpawn;
|
bool m_bRejectSpawn;
|
||||||
|
|
|
@ -95,7 +95,10 @@ This feature is passive: it only checks the team sizes when someone tries to joi
|
||||||
### Bots
|
### 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):
|
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_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_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.
|
- `set sv_numbots x`: Set the number of bots to spawn. It will be capped to the value of `sv_maxbots`.
|
||||||
- `addbot x`: x is the number of bots to add.
|
- `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.
|
||||||
- `removebot x`: x is the number of bots to remove.
|
|
||||||
|
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.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue