From c76cdc5903e0b58a52c263844059a8499371e85f Mon Sep 17 00:00:00 2001 From: Marco Hladik Date: Wed, 23 Dec 2020 07:53:43 +0100 Subject: [PATCH] BotLib: Added basic waypointing and pathfinding system. They'll go about their business but not do much else just yet. --- src/botlib/bot.cpp | 212 +++++++++++++++--- src/botlib/bot.h | 18 +- src/botlib/botinfo.h | 23 ++ src/botlib/cmd.c | 58 +++++ src/botlib/defs.h | 19 ++ src/botlib/include.src | 7 +- src/botlib/route.c | 115 ++++++++++ src/botlib/route.h | 20 ++ src/botlib/way.c | 332 ++++++++++++++++++++++++++++ src/server/defs.h | 1 + src/server/entry.c | 70 +++++- src/server/nodes.c | 2 + src/server/nodes.h | 2 +- src/server/valve/bot.cpp | 18 ++ src/server/valve/item_battery.cpp | 1 + src/server/valve/item_healthkit.cpp | 1 + src/server/valve/spawn.c | 4 + 17 files changed, 866 insertions(+), 37 deletions(-) create mode 100644 src/botlib/botinfo.h create mode 100644 src/botlib/cmd.c create mode 100644 src/botlib/defs.h create mode 100644 src/botlib/route.c create mode 100644 src/botlib/route.h create mode 100644 src/botlib/way.c create mode 100644 src/server/valve/bot.cpp diff --git a/src/botlib/bot.cpp b/src/botlib/bot.cpp index fc565488..8727ae66 100644 --- a/src/botlib/bot.cpp +++ b/src/botlib/bot.cpp @@ -15,44 +15,192 @@ */ void -bot::PickName(void) +bot::CheckRoute(void) { - int n = 0; - entity pbot = world; - for (int i = 1; (pbot = edict_num(i)); i++) { - if (clienttype(pbot) == CLIENTTYPE_BOT) { - n++; + float flDist; + vector evenpos; + + if (!m_iNodes) { + return; + } + + /* level out position/node stuff */ + if (m_iCurNode < 0) { + evenpos = m_vecLastNode; + evenpos[2] = origin[2]; + } else { + evenpos = m_pRoute[m_iCurNode].m_vecDest; + evenpos[2] = origin[2]; + } + + flDist = floor(vlen(evenpos - origin)); + + if ( flDist < 16 ) { + dprint(sprintf("^2CBaseMonster::^3CheckRoute^7: " \ + "%s reached node\n", this.targetname)); + m_iCurNode--; + velocity = [0,0,0]; /* clamp friction */ + + /* we've still traveling and from this node we may be able to walk + * directly to our end-destination */ + if (m_iCurNode > -1) { + tracebox(origin, mins, maxs, m_vecLastNode, MOVE_NORMAL, this); + + /* can we walk directly to our target destination? */ + if (trace_fraction == 1.0) { + dprint("^2CBaseMonster::^3CheckRoute^7: " \ + "Walking directly to last node\n"); + m_iCurNode = -1; + } + } + } else { + traceline( origin + view_ofs, m_pRoute[m_iCurNode].m_vecDest, MOVE_NORMAL, this ); + + /* we can't trace against our next waypoint... that should never happen */ + if ( trace_fraction != 1.0f ) { + m_flNodeGiveup += frametime; + } else { + /* if we're literally stuck in a corner aiming at something we should + * not, also give up */ + if ( flDist == m_flLastDist ) { + m_flNodeGiveup += frametime; + } else { + m_flNodeGiveup = bound( 0, m_flNodeGiveup - frametime, 1.0 ); + } } } - forceinfokey(this, "name", sprintf("Bot %i", n)); + + m_flLastDist = flDist; + + if ( m_flNodeGiveup >= 1.0f ) { + dprint(sprintf("bot::CheckRoute: %s gave up route\n", + this.netname)); + + m_iCurNode = -2; + m_flNodeGiveup = 0.0f; + } else if ( m_flNodeGiveup >= 0.5f ) { + input_buttons |= INPUT_BUTTON2; + } + + if (m_iCurNode < -1) { + dprint(sprintf("bot::CheckRoute: %s calculates new route\n", + this.netname)); + + m_iNodes = 0; + memfree( m_pRoute ); + route_calculate( this, Route_SelectDestination( this ), 0, Bot_RouteCB ); + return; + } +} + +void +bot::RunAI(void) +{ + vector aimdir, aimpos; + int enemyvisible, enemydistant; + float flLerp; + + /* reset input frame */ + input_buttons = 0; + input_movevalues = [0,0,0]; + input_angles = [0,0,0]; + + /* attempt to respawn when dead */ + if (health <= 0) { + input_buttons |= INPUT_BUTTON0; + } + + /* create our first route */ + if (!m_iNodes) { + route_calculate(this, Route_SelectDestination(this), 0, Bot_RouteCB); + + dprint(sprintf("bot::RunAI: %s is calculating first bot route\n", + this.netname)); + + /* our route probably has not been processed yet */ + if (!m_iNodes) { + return; + } + } + + //WeaponThink(); + //PickEnemy(); + + enemyvisible = FALSE; + enemydistant = FALSE; + + if (m_eTarget != __NULL__) { + traceline(origin + view_ofs, m_eTarget.origin, TRUE, this); + enemyvisible = (trace_ent == m_eTarget || trace_fraction == 1.0f); + + if (vlen(trace_endpos - origin) > 1024) { + enemydistant = TRUE; + } + + if (enemyvisible) { + //WeaponAttack(); + } + } + + CheckRoute(); + + if (m_iNodes) { + if (!m_eTarget || !enemyvisible) { + /* aim at the next node */ + if (m_iCurNode == -1) + aimpos = m_vecLastNode; + else + aimpos = m_pRoute[m_iCurNode].m_vecDest; + } else { + /* aim towards the enemy */ + aimpos = m_eTarget.origin; + } + + /* lerping speed */ + flLerp = bound(0.0f, 1.0f - (frametime * 15), 1.0f); + + /* that's the old angle */ + makevectors(v_angle); + vector vNewAngles = v_forward; + + /* aimdir = final angle */ + aimdir = vectoangles(aimpos - origin); + makevectors(aimdir); + + /* slowly lerp towards the final angle */ + vNewAngles[0] = Math_Lerp(vNewAngles[0], v_forward[0], flLerp); + vNewAngles[1] = Math_Lerp(vNewAngles[1], v_forward[1], flLerp); + vNewAngles[2] = Math_Lerp(vNewAngles[2], v_forward[2], flLerp); + + /* make sure we're aiming tight */ + v_angle = vectoangles(vNewAngles); + v_angle[0] = Math_FixDelta(v_angle[0]); + v_angle[1] = Math_FixDelta(v_angle[1]); + v_angle[2] = Math_FixDelta(v_angle[2]); + input_angles = v_angle; + angles[0] = Math_FixDelta(v_angle[0]); + angles[1] = Math_FixDelta(v_angle[1]); + angles[2] = Math_FixDelta(v_angle[2]); + + /* now we'll set the movevalues relative to the input_angle */ + vector direction = normalize(aimpos - origin) * 240; + makevectors(input_angles); + input_movevalues = [v_forward * direction, v_right * direction, v_up * direction]; + } + + /* press any buttons needed */ + button0 = input_buttons & INPUT_BUTTON0; //attack + button2 = input_buttons & INPUT_BUTTON2; //jump + button3 = input_buttons & INPUT_BUTTON3; //tertiary + button4 = input_buttons & INPUT_BUTTON4; //reload + button5 = input_buttons & INPUT_BUTTON5; //secondary + button6 = input_buttons & INPUT_BUTTON6; //use + button7 = input_buttons & INPUT_BUTTON7; //unused + button8 = input_buttons & INPUT_BUTTON8; //duck + movement = input_movevalues; } void bot::bot(void) { - PickName(); - ClientConnect(); - PutClientInServer(); -} - -entity -Bot_AddQuick(void) -{ - entity newbot; - entity oself; - - oself = self; - self = world; - self = spawnclient(); - - if (!self) { - print("^1Bot_AddQuick^7: Can't add bot. Server is full\n"); - self = oself; - return world; - } - - spawnfunc_bot(); - newbot = self; - self = oself; - return newbot; } diff --git a/src/botlib/bot.h b/src/botlib/bot.h index 63c11f5e..5af0d9a0 100644 --- a/src/botlib/bot.h +++ b/src/botlib/bot.h @@ -14,6 +14,8 @@ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#define COST_INFINITE 99999 + enum { BOT_PERSONALITY_NORMAL, @@ -23,8 +25,22 @@ enum class bot:player { + /* routing */ + int m_iNodes; + int m_iCurNode; + nodeslist_t *m_pRoute; + float m_flNodeGiveup; + float m_flLastDist; + entity m_eDestination; + vector m_vecLastNode; + + /* combat */ + entity m_eTarget; + void(void) bot; - virtual void(void) PickName; + + virtual void(void) RunAI; + virtual void(void) CheckRoute; }; entity Bot_AddQuick(void); diff --git a/src/botlib/botinfo.h b/src/botlib/botinfo.h new file mode 100644 index 00000000..23ebda1f --- /dev/null +++ b/src/botlib/botinfo.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +enum +{ + BOTINFO_HEALTH, + BOTINFO_ARMOR, + BOTINFO_SPAWNPOINT, + BOTINFO_END +}; diff --git a/src/botlib/cmd.c b/src/botlib/cmd.c new file mode 100644 index 00000000..585e9d9a --- /dev/null +++ b/src/botlib/cmd.c @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +void +Bot_PickName(entity target) +{ + int n = 0; + entity pbot = world; + for (int i = 1; (pbot = edict_num(i)); i++) { + if (clienttype(pbot) == CLIENTTYPE_BOT) { + n++; + } + } + forceinfokey(target, "name", sprintf("Bot %i", n)); +} + +entity +Bot_AddQuick(void) +{ + /* we've got no nodes, so generate some fake waypoints */ + if (!g_nodes_present) { + return world; + } + + entity newbot; + entity oself; + + oself = self; + self = world; + self = spawnclient(); + + if (!self) { + print("^1Bot_AddQuick^7: Can't add bot. Server is full\n"); + self = oself; + return world; + } + + Bot_PickName(self); + ClientConnect(); + PutClientInServer(); + + newbot = self; + self = oself; + return newbot; +} diff --git a/src/botlib/defs.h b/src/botlib/defs.h new file mode 100644 index 00000000..be10ebcc --- /dev/null +++ b/src/botlib/defs.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +#include "bot.h" +#include "botinfo.h" +#include "route.h" diff --git a/src/botlib/include.src b/src/botlib/include.src index 5cc63c7c..af9ca617 100644 --- a/src/botlib/include.src +++ b/src/botlib/include.src @@ -1,6 +1,9 @@ #define BOT_INCLUDED #includelist -../botlib/bot.h -../botlib/bot.cpp +defs.h +bot.cpp +route.c +way.c +cmd.c #endlist diff --git a/src/botlib/route.c b/src/botlib/route.c new file mode 100644 index 00000000..0b6b0c93 --- /dev/null +++ b/src/botlib/route.c @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +/* +* Begin calculating a route. +* The callback function will be called once the route has finished being calculated. +* The route must be memfreed once it is no longer needed. +* The route must be followed in reverse order (ie: the first node that must be reached +* is at index numnodes-1). If no route is available then the callback will be called with no nodes. +*/ + +int Route_RoundDistance( float flDist ) +{ + float r = fabs( flDist ) % 2; + if ( r == 0 ) { + return flDist; + } + + if ( flDist < 0 ) { + return -( fabs( flDist ) - r ); + } else { + return flDist + 2 - r; + } +} + + +/* +================ +Spawn_SelectRandom +================ +*/ +entity Route_SelectRandom ( string sEntname ) +{ + static entity eLastSpot; + eLastSpot = find( eLastSpot, classname, sEntname ); + return eLastSpot; +} + +/* +================ +Route_SelectRandomSpot +================ +*/ +entity Route_SelectRandomSpot(void) +{ + static entity eLastSpot; + eLastSpot = findfloat( eLastSpot, ::botinfo, BOTINFO_SPAWNPOINT ); + return eLastSpot; +} + +void Bot_RouteCB( entity ent, vector dest, int numnodes, nodeslist_t *nodelist ) +{ + bot b = (bot)ent; + b.m_iNodes = numnodes; + b.m_iCurNode = numnodes - 1; + b.m_pRoute = nodelist; + b.m_vecLastNode = dest; + + //dprint( "Bot: Route calculated.\n" ); + //dprint( sprintf( "Bot: # of nodes: %i\n", bot.m_iNodes ) ); + //dprint( sprintf( "Bot: # current node: %i\n", bot.m_iCurNode ) ); + //dprint( sprintf( "Bot: # endpos: %v\n", dest ) ); +} + +vector Route_SelectDestination( bot target ) +{ + entity dest = world; + + // Need health! + if ( target.health < 50 ) { + entity temp; + int bestrange = COST_INFINITE; + int range; + for ( temp = world; ( temp = findfloat( temp, ::botinfo, BOTINFO_HEALTH ) ); ) { + range = vlen( temp.origin - target.origin ); + if ( ( range < bestrange ) && ( temp.solid = SOLID_TRIGGER ) ) { + bestrange = range; + dest = temp; + } + } + + if ( dest ) { + //dprint( "Route: Going for health!" ); + return dest.origin + '0 0 32'; + } + } + + dest = Route_SelectRandomSpot(); + target.m_eDestination = dest; + return dest.origin; +} + +int Route_CanCrouch(bot target, vector endpos) +{ + traceline(target.origin + [0,0,-18], endpos, FALSE, target); + + if ( trace_fraction != 1.0f ) { + return FALSE; + } else { + return TRUE; + } +} diff --git a/src/botlib/route.h b/src/botlib/route.h new file mode 100644 index 00000000..03dc240e --- /dev/null +++ b/src/botlib/route.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +int Route_RoundDistance( float flDist ); +vector Route_SelectDestination( bot target ); +int Route_CanCrouch(bot target, vector endpos); +void Bot_RouteCB( entity ent, vector dest, int numnodes, nodeslist_t *nodelist ); diff --git a/src/botlib/way.c b/src/botlib/way.c new file mode 100644 index 00000000..e8883897 --- /dev/null +++ b/src/botlib/way.c @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +#define COST_INFINITE 99999 + +enumflags +{ + WP_JUMP, /* also implies that the bot must first go behind the wp... */ + WP_CLIMB, + WP_CROUCH, + WP_USE +}; + +typedef struct waypoint_s +{ + vector vecOrigin; + float flRadius; /* used for picking the closest waypoint. aka proximity weight. also relaxes routes inside the area. */ + struct wpneighbour_s + { + int node; + float linkcost; + int iFlags; + } *neighbour; + int iNeighbours; +} waypoint_t; + +static waypoint_t *g_pWaypoints; +static int g_iWaypoints; + +static void +Way_WipeWaypoints(void) +{ + for (int i = 0; i < g_iWaypoints; i++) { + memfree(g_pWaypoints[i].neighbour); + } + + memfree(g_pWaypoints); + g_iWaypoints = 0; +} + +void +Way_DumpWaypoints(string filename) +{ + filestream file = fopen(filename, FILE_WRITE); + + if (file < 0) { + print("RT_DumpWaypoints: unable to open ", filename, "\n"); + return; + } + + fputs(file, sprintf("%i\n", g_iWaypoints)); + + for (int i = 0i; i < g_iWaypoints; i++) { + fputs(file, sprintf("%v %f %i\n", g_pWaypoints[i].vecOrigin, g_pWaypoints[i].flRadius, g_pWaypoints[i].iNeighbours)); + + for(int j = 0i; j < g_pWaypoints[i].iNeighbours; j++) { + fputs(file, sprintf(" %i %f %x\n", g_pWaypoints[i].neighbour[j].node, g_pWaypoints[i].neighbour[j].linkcost, (float)g_pWaypoints[i].neighbour[j].iFlags)); + } + } + + fclose(file); +} + +void +Way_ReadWaypoints(string strFile) +{ + float file = fopen(strFile, FILE_READ); + if (file < 0) { + print("Way_DumpWaypoints: unable to open ", strFile, "\n"); + return; + } + + Way_WipeWaypoints(); + + tokenize(fgets(file)); + g_iWaypoints = stoi(argv(0)); + g_pWaypoints = memalloc(sizeof(*g_pWaypoints) * g_iWaypoints); + + for (int i = 0i; i < g_iWaypoints; i++) { + tokenize(fgets(file)); + g_pWaypoints[i].vecOrigin[0] = stof(argv(0)); + g_pWaypoints[i].vecOrigin[1] = stof(argv(1)); + g_pWaypoints[i].vecOrigin[2] = stof(argv(2)); + g_pWaypoints[i].flRadius = stof(argv(3)); + g_pWaypoints[i].iNeighbours = stoi(argv(4)); + g_pWaypoints[i].neighbour = memalloc(sizeof(*g_pWaypoints[i].neighbour) * g_pWaypoints[i].iNeighbours); + + for (int j = 0i; j < g_pWaypoints[i].iNeighbours; j++) { + tokenize(fgets(file)); + g_pWaypoints[i].neighbour[j].node = stoi(argv(0)); + g_pWaypoints[i].neighbour[j].linkcost = stof(argv(1)); + g_pWaypoints[i].neighbour[j].iFlags = stoh(argv(2)); + } + } + fclose(file); +} + +static void +Way_LinkWaypoints(waypoint_t *wp, waypoint_t *w2) +{ + int w2n = w2 - g_pWaypoints; + for (int i = 0i; i < wp->iNeighbours; i++) { + if (wp->neighbour[i].node == w2n) { + return; + } + } + + int idx = wp->iNeighbours++; + wp->neighbour = memrealloc(wp->neighbour, sizeof(*wp->neighbour), idx, wp->iNeighbours); + local struct wpneighbour_s *n = wp->neighbour+idx; + n->node = w2n; + n->linkcost = vlen(w2->vecOrigin - wp->vecOrigin); + n->iFlags = 0; +} + +static void +Way_AutoLink(waypoint_t *wp) +{ + int wpidx = wp-g_pWaypoints; + + for (int i = 0i; i < g_iWaypoints; i++) { + //don't link to ourself... + if (i == wpidx) { + continue; + } + + //autolink distance cutoff. + if (vlen(wp->vecOrigin - g_pWaypoints[i].vecOrigin) > autocvar(nav_linksize, 256, "Cuttoff distance between links")) { + continue; + } + + //not going to use the full player size because that makes steps really messy. + //however, we do need a little size, for sanity's sake + tracebox(wp->vecOrigin, '-16 -16 0', '16 16 32', g_pWaypoints[i].vecOrigin, TRUE, world); + + //light of sight blocked, don't try autolinking. + if (trace_fraction < 1) { + continue; + } + + Way_LinkWaypoints(wp, &g_pWaypoints[i]); + Way_LinkWaypoints(&g_pWaypoints[i], wp); + } +} + +void +Way_Waypoint_Create(entity ePlayer, int iAutoLink) +{ + int iID = g_iWaypoints++; + g_pWaypoints = memrealloc(g_pWaypoints, sizeof(waypoint_t), iID, g_iWaypoints); + waypoint_t *n = g_pWaypoints + iID; + n->vecOrigin = ePlayer.origin; + n->neighbour = __NULL__; + n->iNeighbours = 0; + + if (iAutoLink) { + Way_AutoLink(n); + } else { + if (iID != 0) { + Way_LinkWaypoints(n, &g_pWaypoints[iID-1]); + } + } +} + +void +Way_Waypoint_CreateSpawns() +{ + for (entity a = world; (a = find(a, ::classname, "info_player_deathmatch"));) { + Way_Waypoint_Create(a, TRUE); + } +} + +void +Way_Waypoint_Delete(int iID) +{ + if (iID < 0i || iID >= g_iWaypoints) { + print("RT_DeleteWaypoint: invalid waypoint\n"); + return; + } + + //wipe the waypoint + memfree(g_pWaypoints[iID].neighbour); + memcpy(g_pWaypoints + iID, g_pWaypoints + iID + 1, (g_iWaypoints - (iID + 1)) * sizeof(*g_pWaypoints)); + g_iWaypoints--; + + //clean up any links to it. + for (int i = 0; i < g_iWaypoints; i++) { + for (int j = 0; j < g_pWaypoints[i].iNeighbours;) { + int l = g_pWaypoints[i].neighbour[j].node; + if (l == iID) { + memcpy(g_pWaypoints[i].neighbour+j, g_pWaypoints[i].neighbour+j+1, (g_pWaypoints[i].iNeighbours-(j+1))*sizeof(*g_pWaypoints[i].neighbour)); + g_pWaypoints[i].iNeighbours--; + continue; + } else if (l > iID) { + g_pWaypoints[i].neighbour[j].node = l-1; + } + j++; + } + } +} + +void +Way_Waypoint_SetRadius(int iID, float flRadValue) +{ + if (iID < 0i || iID >= g_iWaypoints) { + print("RT_Waypoint_SetRadius: invalid waypoint\n"); + return; + } + g_pWaypoints[iID].flRadius = flRadValue; +} + +void +Way_Waypoint_MakeJump(int iID) +{ + if (iID < 0i || iID >= g_iWaypoints) { + print("RT_Waypoint_SetRadius: invalid waypoint\n"); + return; + } + + for (int j = 0i; j < g_pWaypoints[iID].iNeighbours; j++) { + int iTarget = g_pWaypoints[iID].neighbour[j].node; + + for (int b = 0i; b < g_pWaypoints[iTarget].iNeighbours; b++) { + if (g_pWaypoints[iTarget].neighbour[b].node == iID) { + g_pWaypoints[iTarget].neighbour[b].iFlags = WP_JUMP; + } + } + } +} + +int +Way_FindClosestWaypoint(vector vecOrigin) +{ + + /* -1 for no nodes anywhere... */ + int r = -1i; + float flBestDist = COST_INFINITE; + + for (int i = 0i; i < g_iWaypoints; i++) { + float fDist = vlen(g_pWaypoints[i].vecOrigin - vecOrigin) - g_pWaypoints[i].flRadius; + if (fDist < flBestDist) { + /* within the waypoint's radius */ + if (fDist < 0) { + flBestDist = fDist; + r = i; + } else { + /* outside the waypoint, make sure its valid. */ + traceline(vecOrigin, g_pWaypoints[i].vecOrigin, TRUE, world); + if (trace_fraction == 1) { + /* FIXME: sort them frst, to avoid traces? */ + flBestDist = fDist; + r = i; + } + } + } + } + return r; +} + +void +Way_DrawDebugInfo(void) +{ + if (!g_iWaypoints) { + return; + } + + int iNearest = Way_FindClosestWaypoint(self.origin); + makevectors(self.v_angle); + R_BeginPolygon("textures/dev/info_node", 0, 0); + + for (int i = 0i; i < g_iWaypoints; i++) { + waypoint_t *w = g_pWaypoints + i; + vector org = w->vecOrigin; + vector rgb = [1,1,1]; + + if (iNearest == i) { + rgb = [0,1,0]; + } + + R_PolygonVertex(org + v_right * 8 - v_up * 8, '1 1', rgb, 1); + R_PolygonVertex(org - v_right * 8 - v_up * 8, '0 1', rgb, 1); + R_PolygonVertex(org - v_right * 8 + v_up * 8, [0,0], rgb, 1); + R_PolygonVertex(org + v_right * 8 + v_up * 8, '1 0', rgb, 1); + R_EndPolygon(); + } + + R_BeginPolygon("", 0, 0); + for (int i = 0i; i < g_iWaypoints; i++) { + waypoint_t *w = g_pWaypoints + i; + vector org = w->vecOrigin; + + for (float j = 0; j < (2 * M_PI); j += (2 * M_PI) / 4) { + R_PolygonVertex(org + [sin(j), cos(j)]*w->flRadius, '1 1', '0 0.25 0', 1); + } + R_EndPolygon(); + } + + R_BeginPolygon("", 1, 0); + + for (int i = 0i; i < g_iWaypoints; i++) { + waypoint_t *w = g_pWaypoints+i; + vector org = w->vecOrigin; + vector rgb = [1,1,1]; + + for (int j = 0i; j < w->iNeighbours; j++) { + int k = w->neighbour[j].node; + + if (k < 0i || k >= g_iWaypoints) { + break; + } + + waypoint_t *w2 = &g_pWaypoints[k]; + + R_PolygonVertex(org, '0 1', '1 0 1', 1); + R_PolygonVertex(w2->vecOrigin, '1 1', [0,1,0], 1); + R_EndPolygon(); + } + } +} diff --git a/src/server/defs.h b/src/server/defs.h index 90d86fe5..132a3d87 100644 --- a/src/server/defs.h +++ b/src/server/defs.h @@ -62,6 +62,7 @@ entity g_eAttacker; .float material; .float deaths; .float identity; +.float botinfo; /* in idTech the .owner field causes collisions to fail against set entity, * we don't want this all of the time. so use this as a fallback */ diff --git a/src/server/entry.c b/src/server/entry.c index 0128702d..f661ed8e 100644 --- a/src/server/entry.c +++ b/src/server/entry.c @@ -80,6 +80,12 @@ void PutClientInServer(void) { g_grMode.PlayerSpawn((base_player)self); +#ifdef BOT_INCLUDED + if (clienttype(self) == CLIENTTYPE_BOT) { + spawnfunc_bot(); + } +#endif + /* activate all game_playerspawn entities */ for (entity a = world; (a = find(a, ::targetname, "game_playerspawn"));) { CBaseTrigger t = (CBaseTrigger)a; @@ -113,6 +119,13 @@ void SetChangeParms(void) void SV_RunClientCommand(void) { +#ifdef BOT_INCLUDED + if (time > 5.0) + if (clienttype(self) == CLIENTTYPE_BOT) { + ((bot)self).RunAI(); + } +#endif + if (!Plugin_RunClientCommand()) { Game_RunClientCommand(); } @@ -253,7 +266,18 @@ void worldspawn(void) float ConsoleCmd(string cmd) { - player pl = (player)self; + player pl; + + /* some sv commands can only be executed by a player in-world */ + if ( !self ) { + for ( other = world; ( other = find( other, classname, "player" ) ); ) { + if ( clienttype( other ) == CLIENTTYPE_REAL ) { + self = other; + break; + } + } + } + pl = (player)self; /* give the game-mode a chance to override us */ if (g_grMode.ConsoleCommand(pl, cmd) == TRUE) @@ -274,6 +298,50 @@ float ConsoleCmd(string cmd) t.Trigger(self, TRIG_TOGGLE); } break; +#ifdef BOT_INCLUDED + case "way_add": + if ( !self ) { + return TRUE; + } + Way_Waypoint_Create( self, TRUE ); + break; + case "way_addchain": + if ( !self ) { + return TRUE; + } + Way_Waypoint_Create( self, FALSE ); + break; + case "way_addspawns": + if ( !self ) { + return TRUE; + } + Way_Waypoint_CreateSpawns(); + break; + case "way_delete": + if ( !self ) { + return TRUE; + } + Way_Waypoint_Delete( Way_FindClosestWaypoint( self.origin ) ); + break; + case "way_radius": + if ( !self ) { + return TRUE; + } + Way_Waypoint_SetRadius( Way_FindClosestWaypoint( self.origin ), stof( argv( 1 ) ) ); + break; + case "way_makejump": + if ( !self ) { + return TRUE; + } + Way_Waypoint_MakeJump( Way_FindClosestWaypoint( self.origin ) ); + break; + case "way_save": + Way_DumpWaypoints( argv( 1 ) ); + break; + case "way_load": + Way_ReadWaypoints( argv( 1 ) ); + break; +#endif default: return FALSE; } diff --git a/src/server/nodes.c b/src/server/nodes.c index 4977760a..d9523a8b 100644 --- a/src/server/nodes.c +++ b/src/server/nodes.c @@ -201,6 +201,8 @@ Nodes_Init(void) void SV_AddDebugPolygons(void) { + Way_DrawDebugInfo(); + if (!g_iNodes) { return; } diff --git a/src/server/nodes.h b/src/server/nodes.h index 39a32da3..02fb35d0 100644 --- a/src/server/nodes.h +++ b/src/server/nodes.h @@ -40,4 +40,4 @@ class info_node { }; /* write current nodes to disk */ void Nodes_Save(string); void Nodes_Load(string); -void Nodes_Init(void) +void Nodes_Init(void); diff --git a/src/server/valve/bot.cpp b/src/server/valve/bot.cpp new file mode 100644 index 00000000..dc242272 --- /dev/null +++ b/src/server/valve/bot.cpp @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016-2020 Marco Hladik + * + * 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. + */ + +void +Bot_ diff --git a/src/server/valve/item_battery.cpp b/src/server/valve/item_battery.cpp index b25ea35f..bb19d99e 100644 --- a/src/server/valve/item_battery.cpp +++ b/src/server/valve/item_battery.cpp @@ -70,6 +70,7 @@ void item_battery::Respawn(void) SetSize([-16,-16,0],[16,16,16]); SetOrigin(m_oldOrigin); SetModel(m_oldModel); +// botinfo = BOTINFO_ARMOR; think = __NULL__; nextthink = -1; diff --git a/src/server/valve/item_healthkit.cpp b/src/server/valve/item_healthkit.cpp index c605a32c..a345bc98 100644 --- a/src/server/valve/item_healthkit.cpp +++ b/src/server/valve/item_healthkit.cpp @@ -59,6 +59,7 @@ void item_healthkit::Respawn(void) SetSize([-16,-16,0],[16,16,16]); SetOrigin(m_oldOrigin); SetModel(m_oldModel); + //botinfo = BOTINFO_HEALTH; think = __NULL__; nextthink = -1; diff --git a/src/server/valve/spawn.c b/src/server/valve/spawn.c index 021bcc54..b60cc9e4 100644 --- a/src/server/valve/spawn.c +++ b/src/server/valve/spawn.c @@ -18,20 +18,24 @@ void info_player_start(void) { self.solid = SOLID_TRIGGER; setsize(self, VEC_HULL_MIN, VEC_HULL_MAX); + self.botinfo = BOTINFO_SPAWNPOINT; } void info_player_deathmatch(void) { self.solid = SOLID_TRIGGER; setsize(self, VEC_HULL_MIN, VEC_HULL_MAX); + self.botinfo = BOTINFO_SPAWNPOINT; } void info_player_team1(void) { self.classname = "info_player_deathmatch"; + self.botinfo = BOTINFO_SPAWNPOINT; } void info_player_team2(void) { self.classname = "info_player_deathmatch"; + self.botinfo = BOTINFO_SPAWNPOINT; }