Add a whole bunch of new bot features, including bot profiles.

Developers: the internal class `bot` is now `NSBot`, and an entityDef
of the old name will be instantiated instead.

Override defs/bot.def in your mod and make it use any custom spawnclass
you wish. Now games don't have to override `addbot` or `bot_add` inside
the multiplayer game rules.

There's also more console commands. Clients now have access to:
addBot, killAllBots, killClass [classname], killMovables, trigger [targetname], input [entnum] [input] [data], listBotProfiles, listTargets, teleport [targetname], teleportToClass [classname], respawnEntities, spawn
This commit is contained in:
Marco Cawthorne 2024-01-26 19:22:14 -08:00
parent ba4ddbd3f6
commit 4a8f4a6082
Signed by: eukara
GPG Key ID: CE2032F0A2882A22
18 changed files with 510 additions and 62 deletions

View File

@ -109,9 +109,6 @@ MultiplayerRules::ConsoleCommand(NSClientPlayer pp, string cmd)
tokenize(cmd);
switch (argv(0)) {
case "bot_add":
Bot_AddQuick();
break;
default:
return (0);
}
@ -129,4 +126,4 @@ Game_InitRules(void)
} else {
g_grMode = spawn(MultiplayerRules);
}
}
}

View File

@ -0,0 +1,4 @@
entityDef bot
{
spawnclass NSBot
}

View File

@ -1,5 +1,14 @@
// common
// bots
seta bot_enable 1 // Enable (1) or disable (0) usage of bots in the game.
seta bot_pause 0 // Enable (1) or disable (0) an interrupt for the Bot AIs thinking.
seta bot_noChat 0 // Enable (1) or disable (0) a suppression of any bot chatter.
seta bot_fastChat 0 // Enable (1) or disable (0) bot chatter that does not stop other inputs.
seta bot_debug 0 // Enable (1) or disable (0) bot debug features that otherwise won't work.
seta bot_developer 0 // Enable (1) or disable (0) bot debug text in console.
seta bot_minClients -1 // When set, ensures to fill the server with this many players/bots.
// common
seta com_showFPS 0 // Draws the Frames Per Second counter. Has two values: 1 - Simple 2 - Detailed
seta com_showTracers 0 // Debug display for tracelines, networking intensive.

View File

@ -41,7 +41,7 @@ typedef enum
/** Base class for the Bot AI.
*/
class bot:player
class NSBot:player
{
/* routing */
int m_iNodes;
@ -74,7 +74,7 @@ class bot:player
float m_flForceWeaponAttack;
vector m_vecForceWeaponAttackPos;
void(void) bot;
void(void) NSBot;
virtual void(botstate_t) SetState;
virtual botstate_t(void) GetState;
@ -107,7 +107,7 @@ entity Bot_AddQuick(void);
/** Applies random custom colors to the given bot entity. */
void
Bot_RandomColormap(bot target)
Bot_RandomColormap(NSBot target)
{
vector x = hsv2rgb(random() * 360, 100, 100);
float top = x[2] + (x[1] << 8) + (x[0] << 16);

View File

@ -15,44 +15,44 @@
*/
botstate_t
bot::GetState(void)
NSBot::GetState(void)
{
return m_bsState;
}
void
bot::SetState(botstate_t state)
NSBot::SetState(botstate_t state)
{
m_bsState = state;
}
botpersonality_t
bot::GetPersonality(void)
NSBot::GetPersonality(void)
{
return m_bpPersonality;
}
float
bot::GetWalkSpeed(void)
NSBot::GetWalkSpeed(void)
{
return 120;
}
float
bot::GetRunSpeed(void)
NSBot::GetRunSpeed(void)
{
return 240;
}
void
bot::RouteClear(void)
NSBot::RouteClear(void)
{
super::RouteClear();
m_flNodeGiveup = 0.0f;
}
void
bot::BrainThink(int enemyvisible, int enemydistant)
NSBot::BrainThink(int enemyvisible, int enemydistant)
{
/* we had a target and it's now dead. now what? */
if (m_eTarget) {
@ -67,7 +67,7 @@ bot::BrainThink(int enemyvisible, int enemydistant)
}
void
bot::UseButton(void)
NSBot::UseButton(void)
{
float bestDist;
func_button bestButton = __NULL__;
@ -96,7 +96,7 @@ bot::UseButton(void)
}
void
bot::SeeThink(void)
NSBot::SeeThink(void)
{
NSGameRules rules = (NSGameRules)g_grMode;
@ -163,7 +163,7 @@ bot::SeeThink(void)
}
void
bot::CheckRoute(void)
NSBot::CheckRoute(void)
{
float flDist;
vector vecEndPos;
@ -193,7 +193,7 @@ bot::CheckRoute(void)
/* we're inside the radius */
if (flDist <= flRadius) {
NSLog("^2bot::^3CheckRoute^7: " \
NSLog("^2NSBot::^3CheckRoute^7: " \
"%s reached node\n", this.targetname);
m_iCurNode--;
@ -218,7 +218,7 @@ bot::CheckRoute(void)
/* can we walk directly to our target destination? */
if (trace_fraction == 1.0) {
print("^2bot::^3CheckRoute^7: " \
print("^2NSBot::^3CheckRoute^7: " \
"Walking directly to last node\n");
m_iCurNode = -1;
}
@ -261,13 +261,13 @@ bot::CheckRoute(void)
}
void
bot::CreateObjective(void)
NSBot::CreateObjective(void)
{
RouteToPosition(Route_SelectDestination(this));
}
void
bot::RunAI(void)
NSBot::RunAI(void)
{
vector aimDir, aimPos;
bool enemyVisible, enemyDistant;
@ -294,7 +294,7 @@ bot::RunAI(void)
if (!m_iNodes && autocvar_bot_aimless == 0) {
CreateObjective();
NSLog("bot::RunAI: %s is calculating first bot route",
NSLog("NSBot::RunAI: %s is calculating first bot route",
this.netname);
/* our route probably has not been processed yet */
@ -500,12 +500,12 @@ bot::RunAI(void)
}
void
bot::PreFrame(void)
NSBot::PreFrame(void)
{
}
void
bot::PostFrame(void)
NSBot::PostFrame(void)
{
#ifndef NEW_INVENTORY
/* we've picked something new up */
@ -518,7 +518,7 @@ bot::PostFrame(void)
}
void
bot::SetName(string nickname)
NSBot::SetName(string nickname)
{
if (autocvar_bot_prefix)
forceinfokey(this, "name", sprintf("%s %s", autocvar_bot_prefix, nickname));
@ -527,9 +527,66 @@ bot::SetName(string nickname)
}
void
bot::bot(void)
NSBot::NSBot(void)
{
classname = "player";
targetname = "_nuclide_bot_";
forceinfokey(this, "*bot", "1");
}
void
Bot_KickRandom(void)
{
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
dropclient(e);
return;
}
}
}
void
bot_spawner_think(void)
{
int clientCount = 0i;
int botCount = 0i;
int minClientsCvar = (int)cvar("bot_minClients");
/* if -1, we are not managing _anything_ */
if (minClientsCvar == -1) {
self.nextthink = time + 5.0f;
return;
}
/* count total clients + bots */
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
clientCount++;
botCount++;
}else if (clienttype(e) == CLIENTTYPE_REAL) {
clientCount++;
}
}
/* add remove as necessary */
if (clientCount < cvar("bot_minClients")) {
BotProfile_AddRandom();
} else if (clientCount > cvar("bot_minClients")) {
if (botCount > 0i) {
Bot_KickRandom();
}
}
self.nextthink = time + 1.0f;
}
void
BotLib_Init(void)
{
BotProfile_Init();
/* this spawner manages the active bots */
entity bot_spawner = spawn();
bot_spawner.think = bot_spawner_think;
bot_spawner.nextthink = time + 1.0f;
}

View File

@ -15,13 +15,13 @@
*/
void
bot::ChatSay(string msg)
NSBot::ChatSay(string msg)
{
g_grMode.ChatMessageAll(this, msg);
}
void
bot::ChatSayTeam(string msg)
NSBot::ChatSayTeam(string msg)
{
g_grMode.ChatMessageTeam(this, msg);
}

View File

@ -15,7 +15,7 @@
*/
void
bot::Pain(void)
NSBot::Pain(void)
{
NSGameRules rules = g_grMode;
@ -44,7 +44,7 @@ bot::Pain(void)
}
void
bot::SetEnemy(entity en)
NSBot::SetEnemy(entity en)
{
m_eTarget = en;
@ -56,7 +56,7 @@ bot::SetEnemy(entity en)
}
void
bot::WeaponThink(void)
NSBot::WeaponThink(void)
{
int r = Weapons_IsEmpty(this, activeweapon);
@ -74,7 +74,7 @@ bot::WeaponThink(void)
}
void
bot::WeaponAttack(void)
NSBot::WeaponAttack(void)
{
bool shouldAttack = false;
@ -130,7 +130,7 @@ bot::WeaponAttack(void)
}
void
bot::ForceWeaponAttack(vector attackPos, float attackTime)
NSBot::ForceWeaponAttack(vector attackPos, float attackTime)
{
m_vecForceWeaponAttackPos = attackPos;
m_flForceWeaponAttack = attackTime + time;
@ -153,7 +153,7 @@ BotLib_Alert(vector pos, float radius, float t)
if (vlen(pos - w.origin) > radius)
continue;
bot f = (bot) w;
NSBot f = (NSBot) w;
/* they already got a target of some kind */
if (f.m_eTarget)

View File

@ -61,3 +61,30 @@ Bot_AddQuick(void)
self = oself;
return (newbot);
}
void
Bot_KillAllBots(void)
{
entity oldSelf = self;
for ( other = world; ( other = find( other, classname, "player" ) ); ) {
if ( clienttype( other ) == CLIENTTYPE_BOT ) {
self = other;
ClientKill();
}
}
self = oldSelf;
}
void
Bot_ResetAllBotsGoals(void)
{
for ( other = world; ( other = find( other, classname, "player" ) ); ) {
if ( clienttype( other ) == CLIENTTYPE_BOT ) {
NSBot theBot = (NSBot)other;
theBot.SetEnemy(__NULL__);
theBot.RouteClear();
}
}
}

View File

@ -17,6 +17,7 @@
#include "bot.h"
#include "botinfo.h"
#include "cvar.h"
#include "profiles.h"
vector Route_SelectDestination( bot target );
vector Route_SelectDestination( NSBot target );

View File

@ -2,6 +2,7 @@
#includelist
defs.h
profiles.qc
bot.qc
bot_chat.qc
bot_combat.qc

42
src/botlib/profiles.h Normal file
View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 Vera Visions LLC.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/* BotScript
script/bots.txt
Listing of various bot profiles
where infokeys can be set and interpreted
by the game-logic at will.
The `name` keys has to _always_ be present.
The `funname` key is optional.
Name acts as both an identifier as well
as a nickname when `funname` is not present.
Anything else is considered to be extra.
*/
typedef struct
{
string m_strName;
string m_strNetName;
string m_strExtra;
} botScript_t;
#define BOTSCRIPT_MAX 32
botScript_t g_bots[BOTSCRIPT_MAX];
var int g_botScriptCount;

211
src/botlib/profiles.qc Normal file
View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2023 Vera Visions LLC.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
bool
Bot_ExistsInServer(string botName)
{
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
if (e.netname == botName) {
return (true);
}
}
}
return (false);
}
bool
Bot_AddBot_f(string botName)
{
int extraCount = 0i;
int foundID = -1i;
entity oldSelf;
NSBot newBot;
int i = 0i;
if (!botName) {
print("Usage: Addbot <botname> [skill 1-5] [team] [msec delay] [altname]\n");
return (false);
}
if (!g_nodes_present) {
print("^1BotScript_Add^7: Can't add bot. No waypoints.\n");
return (false);
}
/* grab the right profile id */
for (i = 0i; i < g_botScriptCount; i++) {
if (g_bots[i].m_strName == botName) {
foundID = i;
}
}
if (foundID == -1i) {
print("^1BotScript_Add^7: Named profile not found.\n");
return (false);
}
oldSelf = self;
self = spawnclient();
if (!self) {
print("^1BotScript_Add^7: Can't add bot. Server is full\n");
self = oldSelf;
return (false);
}
newBot = (NSBot)self;
newBot.SetInfoKey("name", g_bots[foundID].m_strNetName);
extraCount = tokenize(g_bots[foundID].m_strExtra);
for (i = 0i; i < extraCount; i+=2) {
newBot.SetInfoKey(argv(i), argv(i+1));
}
ClientConnect();
PutClientInServer();
self = oldSelf;
return (true);
}
bool
BotProfile_AddRandom(void)
{
int startValue = (int)floor(random(0, g_botScriptCount));
int spawnBot = -1i;
/* start at a random index */
for (int i = startValue; i < g_botScriptCount; i++) {
if (Bot_ExistsInServer(g_bots[i].m_strNetName) == false) {
spawnBot = i;
break;
}
}
/* still haven't found it. count down. */
if (spawnBot == -1i) {
for (int i = startValue - 1i; i > 0i; i--) {
if (Bot_ExistsInServer(g_bots[i].m_strNetName) == false) {
spawnBot = i;
break;
}
}
}
/* every bot exists already */
if (spawnBot == -1i) {
print("^1BotProfile_AddRandom^7: Not enough profiles available.\n");
return (false);
}
Bot_AddBot_f(g_bots[spawnBot].m_strName);
return (true);
}
void
BotProfile_Init(void)
{
filestream botScript;
string tempString;
botScript_t currentDef;
int braceDepth = 0i;
g_botScriptCount = 0i;
if (autocvar(bot_enable, 1) == 0) {
return;
}
botScript = fopen("scripts/bots.txt", FILE_READ);
if (botScript < 0) {
return;
}
/* line by line */
while ((tempString = fgets(botScript))) {
int lineSegments = tokenize_console(tempString);
/* word for word */
for (int i = 0i; i < lineSegments; i++) {
string word = argv(i);
switch (word) {
case "{":
braceDepth++;
break;
case "}":
braceDepth--;
/* we've reached the end of a definition */
if (braceDepth == 0) {
/* we have something somewhat valid I guess */
if (currentDef.m_strName != "") {
g_bots[g_botScriptCount].m_strNetName = currentDef.m_strNetName;
g_bots[g_botScriptCount].m_strExtra = currentDef.m_strExtra;
if (g_bots[g_botScriptCount].m_strNetName == "") {
g_bots[g_botScriptCount].m_strNetName = currentDef.m_strName;
}
g_bots[g_botScriptCount].m_strName = strtolower(currentDef.m_strName);
/* increment the def count */
if (g_botScriptCount < BOTSCRIPT_MAX)
g_botScriptCount++;
}
currentDef.m_strName = "";
currentDef.m_strNetName = "";
currentDef.m_strExtra = "";
}
break;
default:
if (braceDepth == 1) {
if (word == "name") {
currentDef.m_strName = argv(i+1);
i++;
} else if (word == "funname") {
currentDef.m_strNetName = argv(i+1);
i++;
} else { /* rest gets dumped into extra */
currentDef.m_strExtra = strcat(currentDef.m_strExtra, "\"", word, "\"", " ");
}
}
}
}
}
fclose(botScript);
print(sprintf("%i bots parsed\n", g_botScriptCount));
}
void
Bot_ListBotProfiles_f(void)
{
if (!g_botScriptCount) {
print("no bot profiles found.\n");
return;
}
for (int i = 0; i < g_botScriptCount; i++) {
print(sprintf("%i: %s\n", i, g_bots[i].m_strName));
print(sprintf("\t%S\n", g_bots[i].m_strNetName));
print(sprintf("\t%s\n", g_bots[i].m_strExtra));
}
}

View File

@ -159,7 +159,7 @@ Route_SelectRandomSpot(void)
}
vector
Route_SelectDestination(bot target)
Route_SelectDestination(NSBot target)
{
CGameRules rules;
rules = (CGameRules)g_grMode;

View File

@ -479,8 +479,8 @@ Way_GoToPoint(entity pl)
for (entity a = world; (a = find(a, classname, "player"));) {
if (clienttype(a) != CLIENTTYPE_REAL) {
bot targ;
targ = (bot)a;
NSBot targ;
targ = (NSBot)a;
targ.RouteClear();
targ.RouteToPosition(pl.origin);
print(sprintf("Told bot to go to %v\n", trace_endpos));

View File

@ -432,6 +432,47 @@ Cmd_Parse(string sCMD)
case "-menu_right":
pSeat->m_iInputReload = FALSE;
break;
/* client aliases for server commands */
case "addBot":
localcmd(sprintf("sv addBot %s\n", argv(1)));
break;
case "killAllBots":
localcmd(sprintf("sv killAllBots %s\n", argv(1)));
break;
case "resetAllBotsGoals":
localcmd(sprintf("sv resetAllBotsGoals %s\n", argv(1)));
break;
case "killClass":
localcmd(sprintf("sv killClass %s\n", argv(1)));
break;
case "killMovables":
localcmd(sprintf("sv killMovables %s\n", argv(1)));
break;
case "trigger":
localcmd(sprintf("sv trigger %s\n", argv(1)));
break;
case "input":
localcmd(sprintf("sv input %s\n", argv(1)));
break;
case "listBotProfiles":
localcmd(sprintf("sv listBotProfiles %s\n", argv(1)));
break;
case "listTargets":
localcmd(sprintf("sv listTargets %s\n", argv(1)));
break;
case "teleport":
localcmd(sprintf("sv teleport %s\n", argv(1)));
break;
case "teleportToClass":
localcmd(sprintf("sv teleportToClass %s\n", argv(1)));
break;
case "respawnEntities":
localcmd(sprintf("sv respawnEntities %s\n", argv(1)));
break;
case "spawn":
localcmd(sprintf("sv spawn %s\n", argv(1)));
break;
default:
return (false);
}
@ -459,6 +500,21 @@ Cmd_Init(void)
registercommand("listClientSoundDef");
registercommand("listServerSoundDef");
/* server commands */
registercommand("addBot");
registercommand("killAllBots");
registercommand("resetAllBotsGoals");
registercommand("killClass");
registercommand("killMovables");
registercommand("trigger");
registercommand("input");
registercommand("listTargets");
registercommand("teleport");
registercommand("teleportToClass");
registercommand("respawnEntities");
registercommand("spawn");
registercommand("listBotProfiles");
registercommand("cleardecals");
registercommand("testLight");
registercommand("testPointLight");

View File

@ -493,10 +493,12 @@ EntityDef_SpawnClassname(string className)
for (int i = 0i; i < g_entDefCount; i++) {
if (className == g_entDefTable[i].entClass) {
EntityDef_Precaches(i);
NSLog("Spawning eDef %S", className);
return EntityDef_PrepareEntity(self, i);
}
}
NSLog("^1Failed spawning eDef %S", className);
return __NULL__;
}

View File

@ -54,7 +54,8 @@ ClientConnect(void)
/* bot needs special init */
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
spawnfunc_bot();
/* from now on we're of type NSBot */
EntityDef_SpawnClassname("bot");
} else
#endif
@ -182,7 +183,7 @@ PlayerPreThink(void)
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
((bot)self).PreFrame();
((NSBot)self).PreFrame();
}
#endif
@ -208,7 +209,7 @@ PlayerPostThink(void)
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
((bot)self).PostFrame();
((NSBot)self).PostFrame();
}
#endif
@ -458,10 +459,14 @@ initents(void)
g_ents_initialized = TRUE;
/* engine hacks for dedicated servers */
cvar_set("s_nominaldistance", "1000");
cvar_set("s_nominaldistance", "2048");
/* other engine hacks */
cvar_set("sv_nqplayerphysics", "0");
#ifdef BOT_INCLUDED
BotLib_Init();
#endif
}
/** Any command executed on the server (either tty, rcon or `sv`) gets
@ -507,12 +512,40 @@ ConsoleCmd(string cmd)
/* time to handle commands that apply to all games */
tokenize(cmd);
switch (argv(0)) {
case "trigger_ent":
case "addBot":
Bot_AddBot_f(strtolower(argv(1)));
break;
case "killAllBots":
Bot_KillAllBots();
break;
case "resetAllBotsGoals":
Bot_ResetAllBotsGoals();
break;
case "listBotProfiles":
Bot_ListBotProfiles_f();
break;
case "killClass":
string targetClass;
targetClass = argv(1);
if (targetClass)
for (entity a = world; (a = find(a, ::classname, targetClass));) {
NSEntity t = (NSEntity)a;
t.Destroy();
}
break;
case "killMovables":
for (entity a = world; (a = findfloat(a, ::movetype, MOVETYPE_PHYSICS));) {
NSEntity t = (NSEntity)a;
t.Destroy();
}
break;
case "trigger":
string targ;
targ = argv(1);
if (targ)
for (entity a = world; (a = find(a, ::targetname, argv(1)));) {
for (entity a = world; (a = find(a, ::targetname, targ));) {
NSEntity t = (NSEntity)a;
if (t.Trigger)
@ -530,34 +563,42 @@ ConsoleCmd(string cmd)
print(sprintf("Sending input to %d, %S: %S\n", entNum, inputName, inputData));
}
break;
case "goto_ent":
case "listTargets":
for (entity a = world; (a = findfloat(a, ::identity, 1));) {
if (a.targetname) {
print(sprintf("%d: %s (%s)\n", num_for_edict(a), a.targetname, a.classname));
}
}
break;
case "teleport":
static entity targetFinder;
targetFinder = find(targetFinder, ::targetname, argv(1));
/* try at least one more time to skip world */
if (!targetFinder)
targetFinder = find(targetFinder, ::targetname, argv(1));
if (targetFinder)
setorigin(pl, targetFinder.origin);
break;
case "teleportToClass":
static entity finder;
finder = find(finder, ::classname, argv(1));
/* try at least one more time to skip world */
if (!finder)
finder = find(finder, ::classname, argv(1));
if (finder)
setorigin(pl, finder.origin);
break;
case "respawn_ents":
case "respawnEntities":
for (entity a = world; (a = findfloat(a, ::identity, 1));) {
NSEntity ent = (NSEntity)a;
ent.Respawn();
}
break;
case "spawn":
entity eDef = spawn();
eDef.classname = strcat("spawnfunc_", argv(1));
self = eDef;
callfunction(self.classname);
self = pl;
makevectors(pl.v_angle);
if (eDef.identity == 1) {
NSEntity ent = (NSEntity)eDef;
}
traceline(pl.origin, pl.origin + (v_forward * 1024), MOVE_NORMAL, pl);
setorigin(eDef, trace_endpos);
break;
case "spawndef":
NSEntity unit = EntityDef_CreateClassname(argv(1));
makevectors(pl.v_angle);
traceline(pl.origin, pl.origin + (v_forward * 1024), MOVE_NORMAL, pl);

View File

@ -666,7 +666,7 @@ NSClientPlayer::ServerInputFrame(void)
#ifdef BOT_INCLUDED
/* wait a few seconds, as we may not have been spawned yet */
if (clienttype(this) == CLIENTTYPE_BOT) {
((bot)this).RunAI();
((NSBot)this).RunAI();
}
#endif