openmohaa/code/fgame/g_bot.cpp

788 lines
16 KiB
C++

/*
===========================================================================
Copyright (C) 2024 the OpenMoHAA team
This file is part of OpenMoHAA source code.
OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
OpenMoHAA 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
// g_bot.cpp
#include "g_local.h"
#include "entity.h"
#include "playerbot.h"
#include "g_bot.h"
static saved_bot_t *saved_bots = NULL;
static unsigned int num_saved_bots = 0;
static unsigned int botId = 0;
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_", 7) || !Q_stricmpn(filename, "american_", 9);
}
/*
===========
IsGermanPlayerModel
Return whether or not the specified filename is for axis
============
*/
bool IsGermanPlayerModel(const char *filename)
{
return !Q_stricmpn(filename, "german_", 7) || !Q_stricmpn(filename, "IT_", 3) || !Q_stricmpn(filename, "SC_", 3);
}
/*
===========
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);
if (len >= 8 && !Q_stricmp(&filename[len - 8], "_fps.tik")) {
return false;
}
if (!IsAlliedPlayerModel(filename) && !IsGermanPlayerModel(filename)) {
return false;
}
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;
int numFiles;
int i;
size_t numAlliedModels = 0, numGermanModels = 0;
byte *p;
ClearModelList();
fileList = gi.FS_ListFiles("models/player", ".tik", qfalse, &numFiles);
for (i = 0; i < numFiles; i++) {
const char *filename = fileList[i];
if (!IsPlayerModel(filename)) {
continue;
}
if (IsAlliedPlayerModel(filename)) {
numAlliedModels++;
} else {
numGermanModels++;
}
}
alliedModelList.Resize(numAlliedModels);
germanModelList.Resize(numGermanModels);
for (i = 0; i < numFiles; i++) {
const char *filename = fileList[i];
size_t len = strlen(filename);
if (!IsPlayerModel(filename)) {
continue;
}
if (IsAlliedPlayerModel(filename)) {
alliedModelList.AddObject(str(filename, 0, len - 4));
} else {
germanModelList.AddObject(str(filename, 0, len - 4));
}
}
gi.FS_FreeFileList(fileList);
}
/*
===========
G_BotBegin
Begin spawning a new bot entity
============
*/
void G_BotBegin(gentity_t *ent)
{
PlayerBot *player;
BotController *controller;
level.spawn_entnum = ent->s.number;
player = new PlayerBot;
G_ClientBegin(ent, NULL);
controller = botManager.getControllerManager().createController(player);
player->setController(controller);
}
/*
===========
G_BotThink
Called each server frame to make bots think
============
*/
void G_BotThink(gentity_t *ent, int msec)
{
/*
usercmd_t ucmd;
usereyes_t eyeinfo;
PlayerBot *bot;
assert(ent);
assert(ent->entity);
assert(ent->entity->IsSubclassOfBot());
bot = (PlayerBot *)ent->entity;
bot->UpdateBotStates();
bot->GetUsercmd(&ucmd);
bot->GetEyeInfo(&eyeinfo);
G_ClientThink(ent, &ucmd, &eyeinfo);
*/
}
/*
===========
G_FindFreeEntityForBot
Find a free client slot
============
*/
gentity_t *G_FindFreeEntityForBot()
{
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;
}
/*
===========
G_ChangeParent
Fix parenting for entities that use the old number
============
*/
void G_ChangeParent(int oldNum, int newNum)
{
gentity_t *ent;
int i;
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_GetRandomAlliedPlayerModel
============
*/
const char* G_GetRandomAlliedPlayerModel()
{
if (!alliedModelList.NumObjects()) {
return "";
}
const unsigned int index = rand() % alliedModelList.NumObjects();
return alliedModelList[index];
}
/*
===========
G_GetRandomGermanPlayerModel
============
*/
const char* G_GetRandomGermanPlayerModel()
{
if (!germanModelList.NumObjects()) {
return "";
}
const unsigned int index = rand() % germanModelList.NumObjects();
return germanModelList[index];
}
/*
===========
G_AddBot
Add the specified bot, optionally its saved state
============
*/
void G_AddBot(const 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;
// increase the unique ID
botId++;
if (saved) {
G_BotConnect(clientNum, qfalse, saved->userinfo);
G_BotBegin(e);
return;
}
if (gi.Argc() > 2) {
Q_strncpyz(botName, gi.Argv(2), sizeof(botName));
} else {
Com_sprintf(botName, sizeof(botName), "bot%d", botId);
}
Info_SetValueForKey(userinfo, "name", botName);
//
// Choose a random model
//
Info_SetValueForKey(userinfo, "dm_playermodel", G_GetRandomAlliedPlayerModel());
Info_SetValueForKey(userinfo, "dm_playergermanmodel", G_GetRandomGermanPlayerModel());
Info_SetValueForKey(userinfo, "fov", "80");
Info_SetValueForKey(userinfo, "ip", "localhost");
// Connect the bot for the first time
// setup user info and stuff
G_BotConnect(clientNum, qtrue, userinfo);
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)
{
if (ent->entity) {
BotController *controller = botManager.getControllerManager().findController(ent->entity);
botManager.getControllerManager().removeController(controller);
}
G_ClientDisconnect(ent);
}
/*
===========
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;
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[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]) {
// Not enough players in that team, don't remove the bot
continue;
}
teamCount[teamIndex]--;
bNoMoreToRemove = false;
}
G_RemoveBot(e);
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()
{
unsigned int count;
unsigned int n;
if (saved_bots) {
delete[] saved_bots;
saved_bots = NULL;
}
const BotControllerManager& manager = botManager.getControllerManager();
unsigned int numSpawnedBots = manager.getControllers().NumObjects();
if (!numSpawnedBots) {
return;
}
saved_bots = new saved_bot_t[numSpawnedBots];
num_saved_bots = 0;
count = manager.getControllers().NumObjects();
assert(count <= numSpawnedBots);
for (n = 1; n <= count; n++) {
const BotController *controller = manager.getControllers().ObjectAt(n);
Player *player = controller->getControlledEntity();
if (!player) {
// this shouldn't happen
continue;
}
saved_bot_t& saved = saved_bots[num_saved_bots++];
memcpy(saved.userinfo, player->client->pers.userinfo, sizeof(saved.userinfo));
}
}
/*
===========
G_RestoreBots
Restore bot persistent data, such as their team
============
*/
void G_RestoreBots()
{
unsigned int n;
if (!saved_bots) {
return;
}
for (n = 0; n < num_saved_bots; n++) {
const saved_bot_t& saved = saved_bots[n];
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 < game.maxclients; n++) {
other = &g_entities[n];
if (G_IsBot(other)) {
continue;
}
if (other->client && other->client->pers.userinfo[0]) {
count++;
}
}
return count;
}
/*
===========
G_ResetBots
Save and reset the bot count
============
*/
void G_ResetBots()
{
G_SaveBots();
botManager.Cleanup();
botId = 0;
}
/*
===========
G_BotInit
Called to initialize bots
============
*/
void G_BotInit()
{
InitModelList();
botManager.Init();
}
/*
===========
G_BotFrame
Called each frame to manage bots
============
*/
void G_BotFrame()
{
botManager.Frame();
}
/*
===========
G_BotPostInit
Called after the server has spawned
============
*/
void G_BotPostInit()
{
G_RestoreBots();
G_SpawnBots();
}
/*
===========
G_SpawnBots
Called each frame to manage bot spawning
============
*/
void G_SpawnBots()
{
unsigned int numClients;
unsigned int numBotsToSpawn;
unsigned int numSpawnedBots;
//
// Check the minimum bot count
//
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);
}
numSpawnedBots = botManager.getControllerManager().getControllers().NumObjects();
//
// Spawn bots
//
if (numBotsToSpawn > numSpawnedBots) {
G_AddBots(numBotsToSpawn - numSpawnedBots);
} else if (numBotsToSpawn < numSpawnedBots) {
G_RemoveBots(numSpawnedBots - numBotsToSpawn);
}
}