merge changes from the defunct ubot
This commit is contained in:
parent
1904b22977
commit
cf501b75b7
90 changed files with 11977 additions and 3907 deletions
257
src/android.cpp
Normal file
257
src/android.cpp
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2020 YaPB Development Team <team@yapb.ru>.
|
||||
//
|
||||
// This program 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
// until hook code will be compatible with ARM, it's here
|
||||
#if defined (CR_ANDROID) && defined(CR_ARCH_ARM)
|
||||
|
||||
CR_EXPORT int Server_GetBlendingInterface (int version, struct sv_blending_interface_s **ppinterface, struct engine_studio_api_s *pstudio, float *rotationmatrix, float *bonetransform) {
|
||||
// this function synchronizes the studio model animation blending interface (i.e, what parts
|
||||
// of the body move, which bones, which hitboxes and how) between the server and the game DLL.
|
||||
// some MODs can be using a different hitbox scheme than the standard one.
|
||||
|
||||
auto api_GetBlendingInterface = game.lib ().resolve <decltype (&Server_GetBlendingInterface)> (__FUNCTION__);
|
||||
|
||||
if (!api_GetBlendingInterface) {
|
||||
logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __FUNCTION__);
|
||||
return false;
|
||||
}
|
||||
return api_GetBlendingInterface (version, ppinterface, pstudio, rotationmatrix, bonetransform);
|
||||
}
|
||||
|
||||
void android_LinkEntity (EntityFunction &addr, const char *name, entvars_t *pev) {
|
||||
if (!addr) {
|
||||
addr = game.lib ().resolve <EntityFunction> (name);
|
||||
}
|
||||
if (!addr) {
|
||||
return;
|
||||
}
|
||||
addr (pev);
|
||||
}
|
||||
|
||||
#define LINK_ENTITY(entityName) \
|
||||
CR_EXPORT void entityName (entvars_t *pev) { \
|
||||
static EntityFunction addr; \
|
||||
android_LinkEntity (addr, #entityName, pev); \
|
||||
}
|
||||
#else
|
||||
#define LINK_ENTITY(entityName)
|
||||
#endif
|
||||
|
||||
// entities in counter-strike...
|
||||
LINK_ENTITY (DelayedUse)
|
||||
LINK_ENTITY (ambient_generic)
|
||||
LINK_ENTITY (ammo_338magnum)
|
||||
LINK_ENTITY (ammo_357sig)
|
||||
LINK_ENTITY (ammo_45acp)
|
||||
LINK_ENTITY (ammo_50ae)
|
||||
LINK_ENTITY (ammo_556nato)
|
||||
LINK_ENTITY (ammo_556natobox)
|
||||
LINK_ENTITY (ammo_57mm)
|
||||
LINK_ENTITY (ammo_762nato)
|
||||
LINK_ENTITY (ammo_9mm)
|
||||
LINK_ENTITY (ammo_buckshot)
|
||||
LINK_ENTITY (armoury_entity)
|
||||
LINK_ENTITY (beam)
|
||||
LINK_ENTITY (bodyque)
|
||||
LINK_ENTITY (button_target)
|
||||
LINK_ENTITY (cycler)
|
||||
LINK_ENTITY (cycler_prdroid)
|
||||
LINK_ENTITY (cycler_sprite)
|
||||
LINK_ENTITY (cycler_weapon)
|
||||
LINK_ENTITY (cycler_wreckage)
|
||||
LINK_ENTITY (env_beam)
|
||||
LINK_ENTITY (env_beverage)
|
||||
LINK_ENTITY (env_blood)
|
||||
LINK_ENTITY (env_bombglow)
|
||||
LINK_ENTITY (env_bubbles)
|
||||
LINK_ENTITY (env_debris)
|
||||
LINK_ENTITY (env_explosion)
|
||||
LINK_ENTITY (env_fade)
|
||||
LINK_ENTITY (env_funnel)
|
||||
LINK_ENTITY (env_global)
|
||||
LINK_ENTITY (env_glow)
|
||||
LINK_ENTITY (env_laser)
|
||||
LINK_ENTITY (env_lightning)
|
||||
LINK_ENTITY (env_message)
|
||||
LINK_ENTITY (env_rain)
|
||||
LINK_ENTITY (env_render)
|
||||
LINK_ENTITY (env_shake)
|
||||
LINK_ENTITY (env_shooter)
|
||||
LINK_ENTITY (env_snow)
|
||||
LINK_ENTITY (env_sound)
|
||||
LINK_ENTITY (env_spark)
|
||||
LINK_ENTITY (env_sprite)
|
||||
LINK_ENTITY (fireanddie)
|
||||
LINK_ENTITY (func_bomb_target)
|
||||
LINK_ENTITY (func_breakable)
|
||||
LINK_ENTITY (func_button)
|
||||
LINK_ENTITY (func_buyzone)
|
||||
LINK_ENTITY (func_conveyor)
|
||||
LINK_ENTITY (func_door)
|
||||
LINK_ENTITY (func_door_rotating)
|
||||
LINK_ENTITY (func_escapezone)
|
||||
LINK_ENTITY (func_friction)
|
||||
LINK_ENTITY (func_grencatch)
|
||||
LINK_ENTITY (func_guntarget)
|
||||
LINK_ENTITY (func_healthcharger)
|
||||
LINK_ENTITY (func_hostage_rescue)
|
||||
LINK_ENTITY (func_illusionary)
|
||||
LINK_ENTITY (func_ladder)
|
||||
LINK_ENTITY (func_monsterclip)
|
||||
LINK_ENTITY (func_mortar_field)
|
||||
LINK_ENTITY (func_pendulum)
|
||||
LINK_ENTITY (func_plat)
|
||||
LINK_ENTITY (func_platrot)
|
||||
LINK_ENTITY (func_pushable)
|
||||
LINK_ENTITY (func_rain)
|
||||
LINK_ENTITY (func_recharge)
|
||||
LINK_ENTITY (func_rot_button)
|
||||
LINK_ENTITY (func_rotating)
|
||||
LINK_ENTITY (func_snow)
|
||||
LINK_ENTITY (func_tank)
|
||||
LINK_ENTITY (func_tankcontrols)
|
||||
LINK_ENTITY (func_tanklaser)
|
||||
LINK_ENTITY (func_tankmortar)
|
||||
LINK_ENTITY (func_tankrocket)
|
||||
LINK_ENTITY (func_trackautochange)
|
||||
LINK_ENTITY (func_trackchange)
|
||||
LINK_ENTITY (func_tracktrain)
|
||||
LINK_ENTITY (func_train)
|
||||
LINK_ENTITY (func_traincontrols)
|
||||
LINK_ENTITY (func_vehicle)
|
||||
LINK_ENTITY (func_vehiclecontrols)
|
||||
LINK_ENTITY (func_vip_safetyzone)
|
||||
LINK_ENTITY (func_wall)
|
||||
LINK_ENTITY (func_wall_toggle)
|
||||
LINK_ENTITY (func_water)
|
||||
LINK_ENTITY (func_weaponcheck)
|
||||
LINK_ENTITY (game_counter)
|
||||
LINK_ENTITY (game_counter_set)
|
||||
LINK_ENTITY (game_end)
|
||||
LINK_ENTITY (game_player_equip)
|
||||
LINK_ENTITY (game_player_hurt)
|
||||
LINK_ENTITY (game_player_team)
|
||||
LINK_ENTITY (game_score)
|
||||
LINK_ENTITY (game_team_master)
|
||||
LINK_ENTITY (game_team_set)
|
||||
LINK_ENTITY (game_text)
|
||||
LINK_ENTITY (game_zone_player)
|
||||
LINK_ENTITY (gibshooter)
|
||||
LINK_ENTITY (grenade)
|
||||
LINK_ENTITY (hostage_entity)
|
||||
LINK_ENTITY (info_bomb_target)
|
||||
LINK_ENTITY (info_hostage_rescue)
|
||||
LINK_ENTITY (info_intermission)
|
||||
LINK_ENTITY (info_landmark)
|
||||
LINK_ENTITY (info_map_parameters)
|
||||
LINK_ENTITY (info_null)
|
||||
LINK_ENTITY (info_player_deathmatch)
|
||||
LINK_ENTITY (info_player_start)
|
||||
LINK_ENTITY (info_target)
|
||||
LINK_ENTITY (info_teleport_destination)
|
||||
LINK_ENTITY (info_vip_start)
|
||||
LINK_ENTITY (infodecal)
|
||||
LINK_ENTITY (item_airtank)
|
||||
LINK_ENTITY (item_airbox)
|
||||
LINK_ENTITY (item_antidote)
|
||||
LINK_ENTITY (item_assaultsuit)
|
||||
LINK_ENTITY (item_battery)
|
||||
LINK_ENTITY (item_healthkit)
|
||||
LINK_ENTITY (item_kevlar)
|
||||
LINK_ENTITY (item_longjump)
|
||||
LINK_ENTITY (item_security)
|
||||
LINK_ENTITY (item_sodacan)
|
||||
LINK_ENTITY (item_suit)
|
||||
LINK_ENTITY (item_thighpack)
|
||||
LINK_ENTITY (light)
|
||||
LINK_ENTITY (light_environment)
|
||||
LINK_ENTITY (light_spot)
|
||||
LINK_ENTITY (momentary_door)
|
||||
LINK_ENTITY (momentary_rot_button)
|
||||
LINK_ENTITY (monster_hevsuit_dead)
|
||||
LINK_ENTITY (monster_mortar)
|
||||
LINK_ENTITY (monster_scientist)
|
||||
LINK_ENTITY (multi_manager)
|
||||
LINK_ENTITY (multisource)
|
||||
LINK_ENTITY (path_corner)
|
||||
LINK_ENTITY (path_track)
|
||||
LINK_ENTITY (player)
|
||||
LINK_ENTITY (player_loadsaved)
|
||||
LINK_ENTITY (player_weaponstrip)
|
||||
LINK_ENTITY (point_clientcommand)
|
||||
LINK_ENTITY (point_servercommand)
|
||||
LINK_ENTITY (soundent)
|
||||
LINK_ENTITY (spark_shower)
|
||||
LINK_ENTITY (speaker)
|
||||
LINK_ENTITY (target_cdaudio)
|
||||
LINK_ENTITY (test_effect)
|
||||
LINK_ENTITY (trigger)
|
||||
LINK_ENTITY (trigger_auto)
|
||||
LINK_ENTITY (trigger_autosave)
|
||||
LINK_ENTITY (trigger_camera)
|
||||
LINK_ENTITY (trigger_cdaudio)
|
||||
LINK_ENTITY (trigger_changelevel)
|
||||
LINK_ENTITY (trigger_changetarget)
|
||||
LINK_ENTITY (trigger_counter)
|
||||
LINK_ENTITY (trigger_endsection)
|
||||
LINK_ENTITY (trigger_gravity)
|
||||
LINK_ENTITY (trigger_hurt)
|
||||
LINK_ENTITY (trigger_monsterjump)
|
||||
LINK_ENTITY (trigger_multiple)
|
||||
LINK_ENTITY (trigger_once)
|
||||
LINK_ENTITY (trigger_push)
|
||||
LINK_ENTITY (trigger_random)
|
||||
LINK_ENTITY (trigger_random_time)
|
||||
LINK_ENTITY (trigger_random_unique)
|
||||
LINK_ENTITY (trigger_relay)
|
||||
LINK_ENTITY (trigger_setorigin)
|
||||
LINK_ENTITY (trigger_teleport)
|
||||
LINK_ENTITY (trigger_transition)
|
||||
LINK_ENTITY (weapon_ak47)
|
||||
LINK_ENTITY (weapon_aug)
|
||||
LINK_ENTITY (weapon_awp)
|
||||
LINK_ENTITY (weapon_c4)
|
||||
LINK_ENTITY (weapon_deagle)
|
||||
LINK_ENTITY (weapon_elite)
|
||||
LINK_ENTITY (weapon_famas)
|
||||
LINK_ENTITY (weapon_fiveseven)
|
||||
LINK_ENTITY (weapon_flashbang)
|
||||
LINK_ENTITY (weapon_g3sg1)
|
||||
LINK_ENTITY (weapon_galil)
|
||||
LINK_ENTITY (weapon_glock18)
|
||||
LINK_ENTITY (weapon_hegrenade)
|
||||
LINK_ENTITY (weapon_knife)
|
||||
LINK_ENTITY (weapon_m249)
|
||||
LINK_ENTITY (weapon_m3)
|
||||
LINK_ENTITY (weapon_m4a1)
|
||||
LINK_ENTITY (weapon_mac10)
|
||||
LINK_ENTITY (weapon_mp5navy)
|
||||
LINK_ENTITY (weapon_p228)
|
||||
LINK_ENTITY (weapon_p90)
|
||||
LINK_ENTITY (weapon_scout)
|
||||
LINK_ENTITY (weapon_sg550)
|
||||
LINK_ENTITY (weapon_sg552)
|
||||
LINK_ENTITY (weapon_shield)
|
||||
LINK_ENTITY (weapon_shieldgun)
|
||||
LINK_ENTITY (weapon_smokegrenade)
|
||||
LINK_ENTITY (weapon_tmp)
|
||||
LINK_ENTITY (weapon_ump45)
|
||||
LINK_ENTITY (weapon_usp)
|
||||
LINK_ENTITY (weapon_xm1014)
|
||||
LINK_ENTITY (weaponbox)
|
||||
LINK_ENTITY (world_items)
|
||||
LINK_ENTITY (worldspawn)
|
||||
5861
src/botlib.cpp
Normal file
5861
src/botlib.cpp
Normal file
File diff suppressed because it is too large
Load diff
371
src/chatlib.cpp
Normal file
371
src/chatlib.cpp
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2020 YaPB Development Team <team@yapb.ru>.
|
||||
//
|
||||
// This program 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_chat ("ub_chat", "1", "Enables or disables bots chat functionality.");
|
||||
|
||||
void BotSupport::stripTags (String &line) {
|
||||
if (line.empty ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &tag : m_tags) {
|
||||
const size_t start = line.find (tag.first, 0);
|
||||
|
||||
if (start != String::InvalidIndex) {
|
||||
const size_t end = line.find (tag.second, start);
|
||||
const size_t diff = end - start;
|
||||
|
||||
if (end != String::InvalidIndex && end > start && diff < 32 && diff > 1) {
|
||||
line.erase (start, diff + tag.second.length ());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BotSupport::humanizePlayerName (String &playerName) {
|
||||
if (playerName.empty ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// drop tag marks, 80 percent of time
|
||||
if (rg.chance (80)) {
|
||||
stripTags (playerName);
|
||||
}
|
||||
else {
|
||||
playerName.trim ();
|
||||
}
|
||||
|
||||
// sometimes switch name to lower characters, only valid for the english languge
|
||||
if (rg.chance (8) && strcmp (cv_language.str (), "en") == 0) {
|
||||
playerName.lowercase ();
|
||||
}
|
||||
}
|
||||
|
||||
void BotSupport::addChatErrors (String &line) {
|
||||
// sometimes switch name to lower characters, only valid for the english languge
|
||||
if (rg.chance (8) && strcmp (cv_language.str (), "en") == 0) {
|
||||
line.lowercase ();
|
||||
}
|
||||
auto length = line.length ();
|
||||
|
||||
if (length > 15) {
|
||||
size_t percentile = line.length () / 2;
|
||||
|
||||
// "length / 2" percent of time drop a character
|
||||
if (rg.chance (percentile)) {
|
||||
line.erase (rg.int_ (length / 8, length - length / 8), 1);
|
||||
}
|
||||
|
||||
// "length" / 4 precent of time swap character
|
||||
if (rg.chance (percentile / 2)) {
|
||||
size_t pos = rg.int_ (length / 8, 3 * length / 8); // choose random position in string
|
||||
cr::swap (line[pos], line[pos + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BotSupport::checkKeywords (StringRef line, String &reply) {
|
||||
// this function checks is string contain keyword, and generates reply to it
|
||||
|
||||
if (!cv_chat.bool_ () || line.empty ()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &factory : conf.getReplies ()) {
|
||||
for (const auto &keyword : factory.keywords) {
|
||||
|
||||
// check is keyword has occurred in message
|
||||
if (line.find (keyword) != String::InvalidIndex) {
|
||||
auto &usedReplies = factory.usedReplies;
|
||||
|
||||
if (usedReplies.length () >= factory.replies.length () / 4) {
|
||||
usedReplies.clear ();
|
||||
}
|
||||
|
||||
if (!factory.replies.empty ()) {
|
||||
bool replyUsed = false;
|
||||
StringRef choosenReply = factory.replies.random ();
|
||||
|
||||
// don't say this twice
|
||||
for (auto &used : usedReplies) {
|
||||
if (used.contains (choosenReply)) {
|
||||
replyUsed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reply not used, so use it
|
||||
if (!replyUsed) {
|
||||
reply.assign (choosenReply); // update final buffer
|
||||
usedReplies.push (choosenReply); // add to ignore list
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// didn't find a keyword? 70% of the time use some universal reply
|
||||
if (rg.chance (70) && conf.hasChatBank (Chat::NoKeyword)) {
|
||||
reply.assign (conf.pickRandomFromChatBank (Chat::NoKeyword));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Bot::prepareChatMessage (StringRef message) {
|
||||
// this function parses messages from the botchat, replaces keywords and converts names into a more human style
|
||||
|
||||
if (!cv_chat.bool_ () || message.empty ()) {
|
||||
return;
|
||||
}
|
||||
m_chatBuffer = message;
|
||||
|
||||
// must be called before return or on the end
|
||||
auto finishPreparation = [&] () {
|
||||
if (!m_chatBuffer.empty ()) {
|
||||
util.addChatErrors (m_chatBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
// need to check if we're have special symbols
|
||||
size_t pos = message.find ('%');
|
||||
|
||||
// nothing found, bail out
|
||||
if (pos == String::InvalidIndex || pos >= message.length ()) {
|
||||
finishPreparation ();
|
||||
return;
|
||||
}
|
||||
|
||||
// get the humanized name out of client
|
||||
auto humanizedName = [] (int index) -> StringRef {
|
||||
auto ent = game.playerOfIndex (index);
|
||||
|
||||
if (!util.isPlayer (ent)) {
|
||||
return "unknown";
|
||||
}
|
||||
String playerName = ent->v.netname.chars ();
|
||||
util.humanizePlayerName (playerName);
|
||||
|
||||
return playerName;
|
||||
};
|
||||
|
||||
// find highfrag player
|
||||
auto getHighfragPlayer = [&] () -> StringRef {
|
||||
int highestFrags = -1;
|
||||
int index = 0;
|
||||
|
||||
for (int i = 0; i < game.maxClients (); ++i) {
|
||||
const Client &client = util.getClient (i);
|
||||
|
||||
if (!(client.flags & ClientFlags::Used) || client.ent == ent ()) {
|
||||
continue;
|
||||
}
|
||||
int frags = static_cast <int> (client.ent->v.frags);
|
||||
|
||||
if (frags > highestFrags) {
|
||||
highestFrags = frags;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
return humanizedName (index);
|
||||
};
|
||||
|
||||
// get roundtime
|
||||
auto getRoundTime = [] () -> StringRef {
|
||||
auto roundTimeSecs = static_cast <int> (bots.getRoundEndTime () - game.time ());
|
||||
|
||||
String roundTime;
|
||||
roundTime.assignf ("%02d:%02d", cr::clamp (roundTimeSecs / 60, 0, 59), cr::clamp (cr::abs (roundTimeSecs % 60), 0, 59));
|
||||
|
||||
return roundTime;
|
||||
};
|
||||
|
||||
// get bot's victim
|
||||
auto getMyVictim = [&] () -> StringRef {;
|
||||
return humanizedName (game.indexOfPlayer (m_lastVictim));
|
||||
};
|
||||
|
||||
// get the game name alias
|
||||
auto getGameName = [] () -> StringRef {
|
||||
String gameName;
|
||||
|
||||
if (game.is (GameFlags::ConditionZero)) {
|
||||
if (rg.chance (30)) {
|
||||
gameName = "CZ";
|
||||
}
|
||||
else {
|
||||
gameName = "Condition Zero";
|
||||
}
|
||||
}
|
||||
else if (game.is (GameFlags::Modern) || game.is (GameFlags::Legacy)) {
|
||||
if (rg.chance (30)) {
|
||||
gameName = "CS";
|
||||
}
|
||||
else {
|
||||
gameName = "Counter-Strike";
|
||||
}
|
||||
}
|
||||
return gameName;
|
||||
};
|
||||
|
||||
// get enemy or teammate alive
|
||||
auto getPlayerAlive = [&] (bool needsEnemy) -> StringRef {
|
||||
for (const auto &client : util.getClients ()) {
|
||||
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent ()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (needsEnemy && m_team != client.team) {
|
||||
return humanizedName (game.indexOfPlayer (client.ent));
|
||||
}
|
||||
else if (!needsEnemy && m_team == client.team) {
|
||||
return humanizedName (game.indexOfPlayer (client.ent));
|
||||
}
|
||||
}
|
||||
return getHighfragPlayer ();
|
||||
};
|
||||
size_t replaceCounter = 0;
|
||||
|
||||
while (replaceCounter < 6 && (pos = m_chatBuffer.find ('%')) != String::InvalidIndex) {
|
||||
// found one, let's do replace
|
||||
switch (m_chatBuffer[pos + 1]) {
|
||||
|
||||
// the highest frag player
|
||||
case 'f':
|
||||
m_chatBuffer.replace ("%f", getHighfragPlayer ());
|
||||
break;
|
||||
|
||||
// current map name
|
||||
case 'm':
|
||||
m_chatBuffer.replace ("%m", game.getMapName ());
|
||||
break;
|
||||
|
||||
// round time
|
||||
case 'r':
|
||||
m_chatBuffer.replace ("%r", getRoundTime ());
|
||||
break;
|
||||
|
||||
// chat reply
|
||||
case 's':
|
||||
m_chatBuffer.replace ("%s", m_sayTextBuffer.entityIndex != -1 ? humanizedName (m_sayTextBuffer.entityIndex) : getHighfragPlayer ());
|
||||
break;
|
||||
|
||||
// last bot victim
|
||||
case 'v':
|
||||
m_chatBuffer.replace ("%v", getMyVictim ());
|
||||
break;
|
||||
|
||||
// game name
|
||||
case 'd':
|
||||
m_chatBuffer.replace ("%d", getGameName ());
|
||||
break;
|
||||
|
||||
// teammate alive
|
||||
case 't':
|
||||
m_chatBuffer.replace ("%t", getPlayerAlive (false));
|
||||
break;
|
||||
|
||||
// enemy alive
|
||||
case 'e':
|
||||
m_chatBuffer.replace ("%e", getPlayerAlive (true));
|
||||
break;
|
||||
};
|
||||
++replaceCounter;
|
||||
}
|
||||
finishPreparation ();
|
||||
}
|
||||
|
||||
bool Bot::checkChatKeywords (String &reply) {
|
||||
// this function parse chat buffer, and prepare buffer to keyword searching
|
||||
|
||||
return util.checkKeywords (utf8tools.strToUpper (m_sayTextBuffer.sayText), reply);
|
||||
}
|
||||
|
||||
bool Bot::isReplyingToChat () {
|
||||
// this function sends reply to a player
|
||||
|
||||
if (m_sayTextBuffer.entityIndex != -1 && !m_sayTextBuffer.sayText.empty ()) {
|
||||
// check is time to chat is good
|
||||
if (m_sayTextBuffer.timeNextChat < game.time () + rg.float_ (m_sayTextBuffer.chatDelay / 2, m_sayTextBuffer.chatDelay)) {
|
||||
String replyText;
|
||||
|
||||
if (rg.chance (m_sayTextBuffer.chatProbability + rg.int_ (20, 50)) && checkChatKeywords (replyText)) {
|
||||
prepareChatMessage (replyText);
|
||||
pushMsgQueue (BotMsg::Say);
|
||||
|
||||
m_sayTextBuffer.entityIndex = -1;
|
||||
m_sayTextBuffer.timeNextChat = game.time () + m_sayTextBuffer.chatDelay;
|
||||
m_sayTextBuffer.sayText.clear ();
|
||||
|
||||
return true;
|
||||
}
|
||||
m_sayTextBuffer.entityIndex = -1;
|
||||
m_sayTextBuffer.sayText.clear ();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Bot::checkForChat () {
|
||||
|
||||
// say a text every now and then
|
||||
if (rg.chance (30) || m_notKilled || !cv_chat.bool_ ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// bot chatting turned on?
|
||||
if (m_lastChatTime + rg.float_ (6.0f, 10.0f) < game.time () && bots.getLastChatTimestamp () + rg.float_ (2.5f, 5.0f) < game.time () && !isReplyingToChat ()) {
|
||||
if (conf.hasChatBank (Chat::Dead)) {
|
||||
StringRef phrase = conf.pickRandomFromChatBank (Chat::Dead);
|
||||
bool sayBufferExists = false;
|
||||
|
||||
// search for last messages, sayed
|
||||
for (auto &sentence : m_sayTextBuffer.lastUsedSentences) {
|
||||
if (phrase.startsWith (sentence)) {
|
||||
sayBufferExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sayBufferExists) {
|
||||
prepareChatMessage (phrase);
|
||||
pushMsgQueue (BotMsg::Say);
|
||||
|
||||
m_lastChatTime = game.time ();
|
||||
bots.setLastChatTimestamp (game.time ());
|
||||
|
||||
// add to ignore list
|
||||
m_sayTextBuffer.lastUsedSentences.push (phrase);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the used line buffer every now and then
|
||||
if (static_cast <int> (m_sayTextBuffer.lastUsedSentences.length ()) > rg.int_ (4, 6)) {
|
||||
m_sayTextBuffer.lastUsedSentences.clear ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Bot::sendToChat (StringRef message, bool teamOnly) {
|
||||
// this function prints saytext message to all players
|
||||
|
||||
if (message.empty () || !cv_chat.bool_ ()) {
|
||||
return;
|
||||
}
|
||||
issueCommand ("%s \"%s\"", teamOnly ? "say_team" : "say", message);
|
||||
}
|
||||
1642
src/combat.cpp
Normal file
1642
src/combat.cpp
Normal file
File diff suppressed because it is too large
Load diff
2118
src/control.cpp
Normal file
2118
src/control.cpp
Normal file
File diff suppressed because it is too large
Load diff
1190
src/engine.cpp
Normal file
1190
src/engine.cpp
Normal file
File diff suppressed because it is too large
Load diff
3046
src/graph.cpp
Normal file
3046
src/graph.cpp
Normal file
File diff suppressed because it is too large
Load diff
957
src/linkage.cpp
Normal file
957
src/linkage.cpp
Normal file
|
|
@ -0,0 +1,957 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2020 YaPB Development Team <team@yapb.ru>.
|
||||
//
|
||||
// This program 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_version ("ub_version", strings.format ("%s.%s", product.version, product.build.count), Var::ReadOnly);
|
||||
|
||||
gamefuncs_t dllapi;
|
||||
enginefuncs_t engfuncs;
|
||||
gamedll_funcs_t dllfuncs;
|
||||
|
||||
meta_globals_t *gpMetaGlobals = nullptr;
|
||||
gamedll_funcs_t *gpGamedllFuncs = nullptr;
|
||||
mutil_funcs_t *gpMetaUtilFuncs = nullptr;
|
||||
globalvars_t *globals = nullptr;
|
||||
|
||||
// metamod plugin information
|
||||
plugin_info_t Plugin_info = {
|
||||
META_INTERFACE_VERSION, // interface version
|
||||
product.name.chars (), // plugin name
|
||||
product.version.chars (), // plugin version
|
||||
product.date.chars (), // date of creation
|
||||
product.author.chars (), // plugin author
|
||||
product.url.chars (), // plugin URL
|
||||
product.logtag.chars (), // plugin logtag
|
||||
PT_CHANGELEVEL, // when loadable
|
||||
PT_ANYTIME, // when unloadable
|
||||
};
|
||||
|
||||
namespace variadic {
|
||||
void clientCommand (edict_t *ent, char const *format, ...) {
|
||||
// this function forces the client whose player entity is ent to issue a client command.
|
||||
// How it works is that clients all have a argv global string in their client DLL that
|
||||
// stores the command string; if ever that string is filled with characters, the client DLL
|
||||
// sends it to the engine as a command to be executed. When the engine has executed that
|
||||
// command, this argv string is reset to zero. Here is somehow a curious implementation of
|
||||
// ClientCommand: the engine sets the command it wants the client to issue in his argv, then
|
||||
// the client DLL sends it back to the engine, the engine receives it then executes the
|
||||
// command therein. Don't ask me why we need all this complicated crap. Anyhow since bots have
|
||||
// no client DLL, be certain never to call this function upon a bot entity, else it will just
|
||||
// make the server crash. Since hordes of uncautious, not to say stupid, programmers don't
|
||||
// even imagine some players on their servers could be bots, this check is performed less than
|
||||
// sometimes actually by their side, that's why we strongly recommend to check it here too. In
|
||||
// case it's a bot asking for a client command, we handle it like we do for bot commands
|
||||
|
||||
va_list ap;
|
||||
auto buffer = strings.chars ();
|
||||
|
||||
va_start (ap, format);
|
||||
vsnprintf (buffer, StringBuffer::StaticBufferSize, format, ap);
|
||||
va_end (ap);
|
||||
|
||||
if (ent && (ent->v.flags & (FL_FAKECLIENT | FL_DORMANT))) {
|
||||
auto bot = bots[ent];
|
||||
|
||||
if (bot) {
|
||||
bot->issueCommand (buffer);
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_SUPERCEDE); // prevent bots to be forced to issue client commands
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnClientCommand (ent, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
|
||||
// this function is called right after GiveFnptrsToDll() by the engine in the game DLL (or
|
||||
// what it BELIEVES to be the game DLL), in order to copy the list of MOD functions that can
|
||||
// be called by the engine, into a memory block pointed to by the functionTable pointer
|
||||
// that is passed into this function (explanation comes straight from botman). This allows
|
||||
// the Half-Life engine to call these MOD DLL functions when it needs to spawn an entity,
|
||||
// connect or disconnect a player, call Think() functions, Touch() functions, or Use()
|
||||
// functions, etc. The bot DLL passes its OWN list of these functions back to the Half-Life
|
||||
// engine, and then calls the MOD DLL's version of GetEntityAPI to get the REAL gamedll
|
||||
// functions this time (to use in the bot code).
|
||||
|
||||
plat.bzero (table, sizeof (gamefuncs_t));
|
||||
|
||||
if (!(game.is (GameFlags::Metamod))) {
|
||||
auto api_GetEntityAPI = game.lib ().resolve <decltype (&GetEntityAPI)> (__FUNCTION__);
|
||||
|
||||
// pass other DLLs engine callbacks to function table...
|
||||
if (!api_GetEntityAPI || api_GetEntityAPI (&dllapi, INTERFACE_VERSION) == 0) {
|
||||
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__);
|
||||
}
|
||||
dllfuncs.dllapi_table = &dllapi;
|
||||
gpGamedllFuncs = &dllfuncs;
|
||||
|
||||
memcpy (table, &dllapi, sizeof (gamefuncs_t));
|
||||
}
|
||||
|
||||
table->pfnGameInit = [] () {
|
||||
// this function is a one-time call, and appears to be the second function called in the
|
||||
// DLL after GiveFntprsToDll() has been called. Its purpose is to tell the MOD DLL to
|
||||
// initialize the game before the engine actually hooks into it with its video frames and
|
||||
// clients connecting. Note that it is a different step than the *server* initialization.
|
||||
// This one is called once, and only once, when the game process boots up before the first
|
||||
// server is enabled. Here is a good place to do our own game session initialization, and
|
||||
// to register by the engine side the server commands we need to administrate our bots.
|
||||
|
||||
// execute main config
|
||||
conf.loadMainConfig ();
|
||||
conf.adjustWeaponPrices ();
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnGameInit ();
|
||||
};
|
||||
|
||||
table->pfnSpawn = [] (edict_t *ent) {
|
||||
// this function asks the game DLL to spawn (i.e, give a physical existence in the virtual
|
||||
// world, in other words to 'display') the entity pointed to by ent in the game. The
|
||||
// Spawn() function is one of the functions any entity is supposed to have in the game DLL,
|
||||
// and any MOD is supposed to implement one for each of its entities.
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, 0);
|
||||
}
|
||||
int result = dllapi.pfnSpawn (ent); // get result
|
||||
|
||||
if (ent->v.rendermode == kRenderTransTexture) {
|
||||
ent->v.flags &= ~FL_WORLDBRUSH; // clear the FL_WORLDBRUSH flag out of transparent ents
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
table->pfnTouch = [] (edict_t *pentTouched, edict_t *pentOther) {
|
||||
// this function is called when two entities' bounding boxes enter in collision. For example,
|
||||
// when a player walks upon a gun, the player entity bounding box collides to the gun entity
|
||||
// bounding box, and the result is that this function is called. It is used by the game for
|
||||
// taking the appropriate action when such an event occurs (in our example, the player who
|
||||
// is walking upon the gun will "pick it up"). Entities that "touch" others are usually
|
||||
// entities having a velocity, as it is assumed that static entities (entities that don't
|
||||
// move) will never touch anything. Hence, in our example, the pentTouched will be the gun
|
||||
// (static entity), whereas the pentOther will be the player (as it is the one moving). When
|
||||
// the two entities both have velocities, for example two players colliding, this function
|
||||
// is called twice, once for each entity moving.
|
||||
|
||||
if (!game.isNullEntity (pentTouched) && pentOther != game.getStartEntity ()) {
|
||||
auto bot = bots[pentTouched];
|
||||
|
||||
if (bot && game.isShootableBreakable (pentOther)) {
|
||||
bot->checkBreakable (pentOther);
|
||||
}
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnTouch (pentTouched, pentOther);
|
||||
};
|
||||
|
||||
table->pfnClientConnect = [] (edict_t *ent, const char *name, const char *addr, char rejectReason[128]) {
|
||||
// this function is called in order to tell the MOD DLL that a client attempts to connect the
|
||||
// game. The entity pointer of this client is ent, the name under which he connects is
|
||||
// pointed to by the pszName pointer, and its IP address string is pointed by the pszAddress
|
||||
// one. Note that this does not mean this client will actually join the game ; he could as
|
||||
// well be refused connection by the server later, because of latency timeout, unavailable
|
||||
// game resources, or whatever reason. In which case the reason why the game DLL (read well,
|
||||
// the game DLL, *NOT* the engine) refuses this player to connect will be printed in the
|
||||
// rejectReason string in all letters. Understand that a client connecting process is done
|
||||
// in three steps. First, the client requests a connection from the server. This is engine
|
||||
// internals. When there are already too many players, the engine will refuse this client to
|
||||
// connect, and the game DLL won't even notice. Second, if the engine sees no problem, the
|
||||
// game DLL is asked. This is where we are. Once the game DLL acknowledges the connection,
|
||||
// the client downloads the resources it needs and synchronizes its local engine with the one
|
||||
// of the server. And then, the third step, which comes *AFTER* ClientConnect (), is when the
|
||||
// client officially enters the game, through the ClientPutInServer () function, later below.
|
||||
// Here we hook this function in order to keep track of the listen server client entity,
|
||||
// because a listen server client always connects with a "loopback" address string. Also we
|
||||
// tell the bot manager to check the bot population, in order to always have one free slot on
|
||||
// the server for incoming clients.
|
||||
|
||||
// check if this client is the listen server client
|
||||
if (strcmp (addr, "loopback") == 0) {
|
||||
game.setLocalEntity (ent); // save the edict of the listen server client...
|
||||
|
||||
// if not dedicated set the default editor for graph
|
||||
if (!game.isDedicated ()) {
|
||||
graph.setEditor (ent);
|
||||
}
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, 0);
|
||||
}
|
||||
return dllapi.pfnClientConnect (ent, name, addr, rejectReason);
|
||||
};
|
||||
|
||||
table->pfnClientDisconnect = [] (edict_t *ent) {
|
||||
// this function is called whenever a client is VOLUNTARILY disconnected from the server,
|
||||
// either because the client dropped the connection, or because the server dropped him from
|
||||
// the game (latency timeout). The effect is the freeing of a client slot on the server. Note
|
||||
// that clients and bots disconnected because of a level change NOT NECESSARILY call this
|
||||
// function, because in case of a level change, it's a server shutdown, and not a normal
|
||||
// disconnection. I find that completely stupid, but that's it. Anyway it's time to update
|
||||
// the bots and players counts, and in case the client disconnecting is a bot, to back its
|
||||
// brain(s) up to disk. We also try to notice when a listenserver client disconnects, so as
|
||||
// to reset his entity pointer for safety. There are still a few server frames to go once a
|
||||
// listen server client disconnects, and we don't want to send him any sort of message then.
|
||||
|
||||
for (auto &bot : bots) {
|
||||
if (bot->pev == &ent->v) {
|
||||
bot->showChaterIcon (false);
|
||||
|
||||
conf.clearUsedName (bot.get ()); // clear the bot name
|
||||
bots.erase (bot.get ()); // remove the bot from bots array
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnClientDisconnect (ent);
|
||||
};
|
||||
|
||||
table->pfnClientUserInfoChanged = [] (edict_t *ent, char *infobuffer) {
|
||||
// this function is called when a player changes model, or changes team. Occasionally it
|
||||
// enforces rules on these changes (for example, some MODs don't want to allow players to
|
||||
// change their player model). But most commonly, this function is in charge of handling
|
||||
// team changes, recounting the teams population, etc...
|
||||
|
||||
ctrl.assignAdminRights (ent, infobuffer);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnClientUserInfoChanged (ent, infobuffer);
|
||||
};
|
||||
|
||||
table->pfnClientCommand = [] (edict_t *ent) {
|
||||
// this function is called whenever the client whose player entity is ent issues a client
|
||||
// command. How it works is that clients all have a global string in their client DLL that
|
||||
// stores the command string; if ever that string is filled with characters, the client DLL
|
||||
// sends it to the engine as a command to be executed. When the engine has executed that
|
||||
// command, that string is reset to zero. By the server side, we can access this string
|
||||
// by asking the engine with the CmdArgv(), CmdArgs() and CmdArgc() functions that work just
|
||||
// like executable files argument processing work in C (argc gets the number of arguments,
|
||||
// command included, args returns the whole string, and argv returns the wanted argument
|
||||
// only). Here is a good place to set up either bot debug commands the listen server client
|
||||
// could type in his game console, or real new client commands, but we wouldn't want to do
|
||||
// so as this is just a bot DLL, not a MOD. The purpose is not to add functionality to
|
||||
// clients. Hence it can lack of commenting a bit, since this code is very subject to change.
|
||||
|
||||
if (ctrl.handleClientCommands (ent)) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_SUPERCEDE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
else if (ctrl.handleMenuCommands (ent)) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_SUPERCEDE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// record stuff about radio and chat
|
||||
bots.captureChatRadio (engfuncs.pfnCmd_Argv (0), engfuncs.pfnCmd_Argv (1), ent);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnClientCommand (ent);
|
||||
};
|
||||
|
||||
table->pfnServerActivate = [] (edict_t *pentEdictList, int edictCount, int clientMax) {
|
||||
// this function is called when the server has fully loaded and is about to manifest itself
|
||||
// on the network as such. Since a mapchange is actually a server shutdown followed by a
|
||||
// restart, this function is also called when a new map is being loaded. Hence it's the
|
||||
// perfect place for doing initialization stuff for our bots, such as reading the BSP data,
|
||||
// loading the bot profiles, and drawing the world map (ie, filling the navigation hashtable).
|
||||
// Once this function has been called, the server can be considered as "running".
|
||||
|
||||
conf.loadConfigs (); // initialize all config files
|
||||
|
||||
// do a level initialization
|
||||
game.levelInitialize (pentEdictList, edictCount);
|
||||
|
||||
// update worldmodel
|
||||
illum.resetWorldModel ();
|
||||
|
||||
// do level initialization stuff here...
|
||||
graph.loadGraphData ();
|
||||
|
||||
// execute main config
|
||||
conf.loadMainConfig ();
|
||||
|
||||
if (File::exists (strings.format ("%s/maps/%s_%s.cfg", game.getModName (), game.getMapName (), product.folder))) {
|
||||
game.serverCommand ("exec maps/%s_%s.cfg", game.getMapName (), product.folder);
|
||||
game.print ("Executing Map-Specific config file");
|
||||
}
|
||||
bots.initQuota ();
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnServerActivate (pentEdictList, edictCount, clientMax);
|
||||
|
||||
graph.rebuildVisibility ();
|
||||
};
|
||||
|
||||
table->pfnServerDeactivate = [] () {
|
||||
// this function is called when the server is shutting down. A particular note about map
|
||||
// changes: changing the map means shutting down the server and starting a new one. Of course
|
||||
// this process is transparent to the user, but either in single player when the hero reaches
|
||||
// a new level and in multiplayer when it's time for a map change, be aware that what happens
|
||||
// is that the server actually shuts down and restarts with a new map. Hence we can use this
|
||||
// function to free and deinit anything which is map-specific, for example we free the memory
|
||||
// space we m'allocated for our BSP data, since a new map means new BSP data to interpret. In
|
||||
// any case, when the new map will be booting, ServerActivate() will be called, so we'll do
|
||||
// the loading of new bots and the new BSP data parsing there.
|
||||
|
||||
// save collected experience on shutdown
|
||||
graph.savePractice ();
|
||||
|
||||
// destroy global killer entity
|
||||
bots.destroyKillerEntity ();
|
||||
|
||||
// set state to unprecached
|
||||
game.setUnprecached ();
|
||||
|
||||
// enable lightstyle animations on level change
|
||||
illum.enableAnimation (true);
|
||||
|
||||
// send message on new map
|
||||
util.setNeedForWelcome (false);
|
||||
|
||||
// xash is not kicking fakeclients on changelevel
|
||||
if (game.is (GameFlags::Xash3D)) {
|
||||
bots.kickEveryone (true, false);
|
||||
}
|
||||
graph.initGraph ();
|
||||
|
||||
// clear all the bots
|
||||
bots.destroy ();
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnServerDeactivate ();
|
||||
};
|
||||
|
||||
table->pfnStartFrame = [] () {
|
||||
// this function starts a video frame. It is called once per video frame by the game. If
|
||||
// you run Half-Life at 90 fps, this function will then be called 90 times per second. By
|
||||
// placing a hook on it, we have a good place to do things that should be done continuously
|
||||
// during the game, for example making the bots think (yes, because no Think() function exists
|
||||
// for the bots by the MOD side, remember). Also here we have control on the bot population,
|
||||
// for example if a new player joins the server, we should disconnect a bot, and if the
|
||||
// player population decreases, we should fill the server with other bots.
|
||||
|
||||
// update lightstyle animations
|
||||
illum.animateLight ();
|
||||
|
||||
// update some stats for clients
|
||||
util.updateClients ();
|
||||
|
||||
if (graph.hasEditFlag (GraphEdit::On) && graph.hasEditor ()) {
|
||||
graph.frame ();
|
||||
}
|
||||
|
||||
// run stuff periodically
|
||||
game.slowFrame ();
|
||||
|
||||
if (bots.hasBotsOnline ()) {
|
||||
// keep track of grenades on map
|
||||
bots.updateActiveGrenade ();
|
||||
|
||||
// keep track of intresting entities
|
||||
bots.updateIntrestingEntities ();
|
||||
}
|
||||
|
||||
// keep bot number up to date
|
||||
bots.maintainQuota ();
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnStartFrame ();
|
||||
|
||||
// run the bot ai
|
||||
bots.frame ();
|
||||
};
|
||||
|
||||
table->pfnCmdStart = [] (const edict_t *player, usercmd_t *cmd, unsigned int random_seed) {
|
||||
auto ent = const_cast <edict_t *> (player);
|
||||
|
||||
// if we're handle pings for bots and clients, clear IN_SCORE button so SV_ShouldUpdatePing engine function return false
|
||||
// and SV_EmitPings will not overwrite our results
|
||||
if (game.is (GameFlags::HasFakePings) && cv_show_latency.int_ () == 2) {
|
||||
if ((cmd->buttons & IN_SCORE) || (ent->v.oldbuttons & IN_SCORE)) {
|
||||
cmd->buttons &= ~IN_SCORE;
|
||||
|
||||
// send our version of pings
|
||||
util.sendPings (ent);
|
||||
}
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnCmdStart (player, cmd, random_seed);
|
||||
};
|
||||
|
||||
table->pfnPM_Move = [] (playermove_t *playerMove, int server) {
|
||||
// this is the player movement code clients run to predict things when the server can't update
|
||||
// them often enough (or doesn't want to). The server runs exactly the same function for
|
||||
// moving players. There is normally no distinction between them, else client-side prediction
|
||||
// wouldn't work properly (and it doesn't work that well, already...)
|
||||
|
||||
illum.setWorldModel (playerMove->physents[0].model);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
dllapi.pfnPM_Move (playerMove, server);
|
||||
};
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_LINKAGE_C int GetEntityAPI_Post (gamefuncs_t *table, int) {
|
||||
// this function is called right after GiveFnptrsToDll() by the engine in the game DLL (or
|
||||
// what it BELIEVES to be the game DLL), in order to copy the list of MOD functions that can
|
||||
// be called by the engine, into a memory block pointed to by the functionTable pointer
|
||||
// that is passed into this function (explanation comes straight from botman). This allows
|
||||
// the Half-Life engine to call these MOD DLL functions when it needs to spawn an entity,
|
||||
// connect or disconnect a player, call Think() functions, Touch() functions, or Use()
|
||||
// functions, etc. The bot DLL passes its OWN list of these functions back to the Half-Life
|
||||
// engine, and then calls the MOD DLL's version of GetEntityAPI to get the REAL gamedll
|
||||
// functions this time (to use in the bot code). Post version, called only by metamod.
|
||||
|
||||
plat.bzero (table, sizeof (gamefuncs_t));
|
||||
|
||||
table->pfnSpawn = [] (edict_t *ent) {
|
||||
// this function asks the game DLL to spawn (i.e, give a physical existence in the virtual
|
||||
// world, in other words to 'display') the entity pointed to by ent in the game. The
|
||||
// Spawn() function is one of the functions any entity is supposed to have in the game DLL,
|
||||
// and any MOD is supposed to implement one for each of its entities. Post version called
|
||||
// only by metamod.
|
||||
|
||||
// solves the bots unable to see through certain types of glass bug.
|
||||
if (ent->v.rendermode == kRenderTransTexture) {
|
||||
ent->v.flags &= ~FL_WORLDBRUSH; // clear the FL_WORLDBRUSH flag out of transparent ents
|
||||
}
|
||||
RETURN_META_VALUE (MRES_HANDLED, 0);
|
||||
};
|
||||
|
||||
table->pfnStartFrame = [] () {
|
||||
// this function starts a video frame. It is called once per video frame by the game. If
|
||||
// you run Half-Life at 90 fps, this function will then be called 90 times per second. By
|
||||
// placing a hook on it, we have a good place to do things that should be done continuously
|
||||
// during the game, for example making the bots think (yes, because no Think() function exists
|
||||
// for the bots by the MOD side, remember). Post version called only by metamod.
|
||||
|
||||
// run the bot ai
|
||||
bots.frame ();
|
||||
|
||||
RETURN_META (MRES_IGNORED);
|
||||
};
|
||||
|
||||
table->pfnServerActivate = [] (edict_t *, int, int) {
|
||||
// this function is called when the server has fully loaded and is about to manifest itself
|
||||
// on the network as such. Since a mapchange is actually a server shutdown followed by a
|
||||
// restart, this function is also called when a new map is being loaded. Hence it's the
|
||||
// perfect place for doing initialization stuff for our bots, such as reading the BSP data,
|
||||
// loading the bot profiles, and drawing the world map (ie, filling the navigation hashtable).
|
||||
// Once this function has been called, the server can be considered as "running". Post version
|
||||
// called only by metamod.
|
||||
|
||||
graph.rebuildVisibility ();
|
||||
|
||||
RETURN_META (MRES_IGNORED);
|
||||
};
|
||||
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
plat.bzero (table, sizeof (enginefuncs_t));
|
||||
}
|
||||
|
||||
table->pfnChangeLevel = [] (char *s1, char *s2) {
|
||||
// the purpose of this function is to ask the engine to shutdown the server and restart a
|
||||
// new one running the map whose name is s1. It is used ONLY IN SINGLE PLAYER MODE and is
|
||||
// transparent to the user, because it saves the player state and equipment and restores it
|
||||
// back in the new level. The "changelevel trigger point" in the old level is linked to the
|
||||
// new level's spawn point using the s2 string, which is formatted as follows: "trigger_name
|
||||
// to spawnpoint_name", without spaces (for example, "tr_1atotr_2lm" would tell the engine
|
||||
// the player has reached the trigger point "tr_1a" and has to spawn in the next level on the
|
||||
// spawn point named "tr_2lm".
|
||||
|
||||
// save collected experience on map change
|
||||
graph.savePractice ();
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnChangeLevel (s1, s2);
|
||||
};
|
||||
|
||||
table->pfnLightStyle = [] (int style, char *val) {
|
||||
// ths function update lightstyle for the bots
|
||||
|
||||
illum.updateLight (style, val);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnLightStyle (style, val);
|
||||
};
|
||||
|
||||
if (game.is (GameFlags::Legacy)) {
|
||||
table->pfnFindEntityByString = [] (edict_t *edictStartSearchAfter, const char *field, const char *value) {
|
||||
// round starts in counter-strike 1.5
|
||||
if (strcmp (value, "info_map_parameters") == 0) {
|
||||
bots.initRound ();
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, static_cast <edict_t *> (nullptr));
|
||||
}
|
||||
return engfuncs.pfnFindEntityByString (edictStartSearchAfter, field, value);
|
||||
};
|
||||
}
|
||||
|
||||
table->pfnEmitSound = [] (edict_t *entity, int channel, const char *sample, float volume, float attenuation, int flags, int pitch) {
|
||||
// this function tells the engine that the entity pointed to by "entity", is emitting a sound
|
||||
// which fileName is "sample", at level "channel" (CHAN_VOICE, etc...), with "volume" as
|
||||
// loudness multiplicator (normal volume VOL_NORM is 1.0), with a pitch of "pitch" (normal
|
||||
// pitch PITCH_NORM is 100.0), and that this sound has to be attenuated by distance in air
|
||||
// according to the value of "attenuation" (normal attenuation ATTN_NORM is 0.8 ; ATTN_NONE
|
||||
// means no attenuation with distance). Optionally flags "fFlags" can be passed, which I don't
|
||||
// know the heck of the purpose. After we tell the engine to emit the sound, we have to call
|
||||
// SoundAttachToThreat() to bring the sound to the ears of the bots. Since bots have no client DLL
|
||||
// to handle this for them, such a job has to be done manually.
|
||||
|
||||
util.listenNoise (entity, sample, volume);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnEmitSound (entity, channel, sample, volume, attenuation, flags, pitch);
|
||||
};
|
||||
|
||||
table->pfnMessageBegin = [] (int msgDest, int msgType, const float *origin, edict_t *ed) {
|
||||
// this function called each time a message is about to sent.
|
||||
|
||||
msgs.start (ed, msgType);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnMessageBegin (msgDest, msgType, origin, ed);
|
||||
};
|
||||
|
||||
if (!game.is (GameFlags::Metamod)) {
|
||||
table->pfnMessageEnd = [] () {
|
||||
engfuncs.pfnMessageEnd ();
|
||||
|
||||
// this allows us to send messages right in handler code
|
||||
msgs.stop ();
|
||||
};
|
||||
}
|
||||
|
||||
table->pfnWriteByte = [] (int value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteByte (value);
|
||||
};
|
||||
|
||||
table->pfnWriteChar = [] (int value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteChar (value);
|
||||
};
|
||||
|
||||
table->pfnWriteShort = [] (int value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteShort (value);
|
||||
};
|
||||
|
||||
table->pfnWriteLong = [] (int value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteLong (value);
|
||||
};
|
||||
|
||||
table->pfnWriteAngle = [] (float value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteAngle (value);
|
||||
};
|
||||
|
||||
table->pfnWriteCoord = [] (float value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteCoord (value);
|
||||
};
|
||||
|
||||
table->pfnWriteString = [] (const char *sz) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (sz);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteString (sz);
|
||||
};
|
||||
|
||||
table->pfnWriteEntity = [] (int value) {
|
||||
// if this message is for a bot, call the client message function...
|
||||
msgs.collect (value);
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnWriteEntity (value);
|
||||
};
|
||||
|
||||
if (!game.is (GameFlags::Metamod)) {
|
||||
table->pfnRegUserMsg = [] (const char *name, int size) {
|
||||
// this function registers a "user message" by the engine side. User messages are network
|
||||
// messages the game DLL asks the engine to send to clients. Since many MODs have completely
|
||||
// different client features (Counter-Strike has a radar and a timer, for example), network
|
||||
// messages just can't be the same for every MOD. Hence here the MOD DLL tells the engine,
|
||||
// "Hey, you have to know that I use a network message whose name is pszName and it is size
|
||||
// packets long". The engine books it, and returns the ID number under which he recorded that
|
||||
// custom message. Thus every time the MOD DLL will be wanting to send a message named pszName
|
||||
// using pfnMessageBegin (), it will know what message ID number to send, and the engine will
|
||||
// know what to do, only for non-metamod version
|
||||
|
||||
return msgs.add (name, engfuncs.pfnRegUserMsg (name, size)); // return privously registered message
|
||||
};
|
||||
}
|
||||
|
||||
table->pfnClientPrintf = [] (edict_t *ent, PRINT_TYPE printType, const char *message) {
|
||||
// this function prints the text message string pointed to by message by the client side of
|
||||
// the client entity pointed to by ent, in a manner depending of printType (print_console,
|
||||
// print_center or print_chat). Be certain never to try to feed a bot with this function,
|
||||
// as it will crash your server. Why would you, anyway ? bots have no client DLL as far as
|
||||
// we know, right ? But since stupidity rules this world, we do a preventive check :)
|
||||
|
||||
if (util.isFakeClient (ent)) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_SUPERCEDE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnClientPrintf (ent, printType, message);
|
||||
};
|
||||
|
||||
table->pfnCmd_Args = [] () {
|
||||
// this function returns a pointer to the whole current client command string. Since bots
|
||||
// have no client DLL and we may want a bot to execute a client command, we had to implement
|
||||
// a argv string in the bot DLL for holding the bots' commands, and also keep track of the
|
||||
// argument count. Hence this hook not to let the engine ask an unexistent client DLL for a
|
||||
// command we are holding here. Of course, real clients commands are still retrieved the
|
||||
// normal way, by asking the game.
|
||||
|
||||
// is this a bot issuing that client command?
|
||||
if (game.isBotCmd ()) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_SUPERCEDE, game.botArgs ());
|
||||
}
|
||||
return game.botArgs (); // else return the whole bot client command string we know
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, static_cast <const char *> (nullptr));
|
||||
}
|
||||
return engfuncs.pfnCmd_Args (); // ask the client command string to the engine
|
||||
};
|
||||
|
||||
table->pfnCmd_Argv = [] (int argc) {
|
||||
// this function returns a pointer to a certain argument of the current client command. Since
|
||||
// bots have no client DLL and we may want a bot to execute a client command, we had to
|
||||
// implement a argv string in the bot DLL for holding the bots' commands, and also keep
|
||||
// track of the argument count. Hence this hook not to let the engine ask an unexistent client
|
||||
// DLL for a command we are holding here. Of course, real clients commands are still retrieved
|
||||
// the normal way, by asking the game.
|
||||
|
||||
// is this a bot issuing that client command?
|
||||
if (game.isBotCmd ()) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_SUPERCEDE, game.botArgv (argc));
|
||||
}
|
||||
return game.botArgv (argc); // if so, then return the wanted argument we know
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, static_cast <const char *> (nullptr));
|
||||
}
|
||||
return engfuncs.pfnCmd_Argv (argc); // ask the argument number "argc" to the engine
|
||||
};
|
||||
|
||||
table->pfnCmd_Argc = [] () {
|
||||
// this function returns the number of arguments the current client command string has. Since
|
||||
// bots have no client DLL and we may want a bot to execute a client command, we had to
|
||||
// implement a argv string in the bot DLL for holding the bots' commands, and also keep
|
||||
// track of the argument count. Hence this hook not to let the engine ask an unexistent client
|
||||
// DLL for a command we are holding here. Of course, real clients commands are still retrieved
|
||||
// the normal way, by asking the game.
|
||||
|
||||
// is this a bot issuing that client command?
|
||||
if (game.isBotCmd ()) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_SUPERCEDE, game.botArgc ());
|
||||
}
|
||||
return game.botArgc (); // if so, then return the argument count we know
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META_VALUE (MRES_IGNORED, 0);
|
||||
}
|
||||
return engfuncs.pfnCmd_Argc (); // ask the engine how many arguments there are
|
||||
};
|
||||
|
||||
table->pfnSetClientMaxspeed = [] (const edict_t *ent, float newMaxspeed) {
|
||||
auto bot = bots[const_cast <edict_t *> (ent)];
|
||||
|
||||
// check wether it's not a bot
|
||||
if (bot != nullptr) {
|
||||
bot->pev->maxspeed = newMaxspeed;
|
||||
}
|
||||
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
RETURN_META (MRES_IGNORED);
|
||||
}
|
||||
engfuncs.pfnSetClientMaxspeed (ent, newMaxspeed);
|
||||
};
|
||||
|
||||
table->pfnClientCommand = variadic::clientCommand;
|
||||
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion) {
|
||||
// it appears that an extra function table has been added in the engine to gamedll interface
|
||||
// since the date where the first enginefuncs table standard was frozen. These ones are
|
||||
// facultative and we don't hook them, but since some MODs might be featuring it, we have to
|
||||
// pass them too, else the DLL interfacing wouldn't be complete and the game possibly wouldn't
|
||||
// run properly.
|
||||
|
||||
auto api_GetNewDLLFunctions = game.lib ().resolve <decltype (&GetNewDLLFunctions)> (__FUNCTION__);
|
||||
|
||||
if (!api_GetNewDLLFunctions || !api_GetNewDLLFunctions (table, interfaceVersion)) {
|
||||
logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __FUNCTION__);
|
||||
return HLFalse;
|
||||
}
|
||||
|
||||
dllfuncs.newapi_table = table;
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_LINKAGE_C int GetEngineFunctions_Post (enginefuncs_t *table, int *) {
|
||||
plat.bzero (table, sizeof (enginefuncs_t));
|
||||
|
||||
table->pfnMessageEnd = [] () {
|
||||
msgs.stop (); // this allows us to send messages right in handler code
|
||||
|
||||
RETURN_META (MRES_IGNORED);
|
||||
};
|
||||
|
||||
table->pfnRegUserMsg = [] (const char *name, int) {
|
||||
// this function registers a "user message" by the engine side. User messages are network
|
||||
// messages the game DLL asks the engine to send to clients. Since many MODs have completely
|
||||
// different client features (Counter-Strike has a radar and a timer, for example), network
|
||||
// messages just can't be the same for every MOD. Hence here the MOD DLL tells the engine,
|
||||
// "Hey, you have to know that I use a network message whose name is pszName and it is size
|
||||
// packets long". The engine books it, and returns the ID number under which he recorded that
|
||||
// custom message. Thus every time the MOD DLL will be wanting to send a message named pszName
|
||||
// using pfnMessageBegin (), it will know what message ID number to send, and the engine will
|
||||
// know what to do, only for non-metamod version
|
||||
|
||||
// register message for our needs
|
||||
msgs.add (name, META_RESULT_ORIG_RET (int));
|
||||
|
||||
RETURN_META_VALUE (MRES_IGNORED, 0);
|
||||
};
|
||||
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_EXPORT int Meta_Query (char *, plugin_info_t **pPlugInfo, mutil_funcs_t *pMetaUtilFuncs) {
|
||||
// this function is the first function ever called by metamod in the plugin DLL. Its purpose
|
||||
// is for metamod to retrieve basic information about the plugin, such as its meta-interface
|
||||
// version, for ensuring compatibility with the current version of the running metamod.
|
||||
|
||||
gpMetaUtilFuncs = pMetaUtilFuncs;
|
||||
*pPlugInfo = &Plugin_info;
|
||||
|
||||
return HLTrue; // tell metamod this plugin looks safe
|
||||
}
|
||||
|
||||
CR_EXPORT int Meta_Attach (PLUG_LOADTIME now, metamod_funcs_t *functionTable, meta_globals_t *pMGlobals, gamedll_funcs_t *pGamedllFuncs) {
|
||||
// this function is called when metamod attempts to load the plugin. Since it's the place
|
||||
// where we can tell if the plugin will be allowed to run or not, we wait until here to make
|
||||
// our initialization stuff, like registering CVARs and dedicated server commands.
|
||||
|
||||
// metamod engine & dllapi function tables
|
||||
static metamod_funcs_t metamodFunctionTable = {
|
||||
GetEntityAPI, // pfnGetEntityAPI ()
|
||||
GetEntityAPI_Post, // pfnGetEntityAPI_Post ()
|
||||
nullptr, // pfnGetEntityAPI2 ()
|
||||
nullptr, // pfnGetEntityAPI2_Post ()
|
||||
nullptr, // pfnGetNewDLLFunctions ()
|
||||
nullptr, // pfnGetNewDLLFunctions_Post ()
|
||||
GetEngineFunctions, // pfnGetEngineFunctions ()
|
||||
GetEngineFunctions_Post, // pfnGetEngineFunctions_Post ()
|
||||
};
|
||||
|
||||
if (now > Plugin_info.loadable) {
|
||||
logger.error ("%s: plugin NOT attaching (can't load plugin right now)", Plugin_info.name);
|
||||
return HLFalse; // returning FALSE prevents metamod from attaching this plugin
|
||||
}
|
||||
|
||||
// keep track of the pointers to engine function tables metamod gives us
|
||||
gpMetaGlobals = pMGlobals;
|
||||
memcpy (functionTable, &metamodFunctionTable, sizeof (metamod_funcs_t));
|
||||
gpGamedllFuncs = pGamedllFuncs;
|
||||
|
||||
return HLTrue; // returning true enables metamod to attach this plugin
|
||||
}
|
||||
|
||||
CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) {
|
||||
// this function is called when metamod unloads the plugin. A basic check is made in order
|
||||
// to prevent unloading the plugin if its processing should not be interrupted.
|
||||
|
||||
if (now > Plugin_info.unloadable && reason != PNL_CMD_FORCED) {
|
||||
logger.error ("%s: plugin NOT detaching (can't unload plugin right now)", Plugin_info.name);
|
||||
return HLFalse; // returning FALSE prevents metamod from unloading this plugin
|
||||
}
|
||||
bots.kickEveryone (true); // kick all bots off this server
|
||||
|
||||
// save collected experience on shutdown
|
||||
graph.savePractice ();
|
||||
util.disableSendTo ();
|
||||
|
||||
// make sure all stuff cleared
|
||||
bots.destroy ();
|
||||
|
||||
return HLTrue;
|
||||
}
|
||||
|
||||
CR_EXPORT void Meta_Init () {
|
||||
// this function is called by metamod, before any other interface functions. Purpose of this
|
||||
// function to give plugin a chance to determine is plugin running under metamod or not.
|
||||
|
||||
game.addGameFlag (GameFlags::Metamod);
|
||||
}
|
||||
|
||||
// games GiveFnptrsToDll is a bit tricky
|
||||
#if defined(CR_WINDOWS)
|
||||
# if defined(CR_CXX_MSVC) || defined (CR_CXX_CLANG)
|
||||
# if defined (CR_ARCH_X86)
|
||||
# pragma comment(linker, "/EXPORT:GiveFnptrsToDll=_GiveFnptrsToDll@8,@1")
|
||||
# endif
|
||||
# pragma comment(linker, "/SECTION:.data,RW")
|
||||
# endif
|
||||
# if defined(CR_CXX_MSVC) && !defined(CR_ARCH_X64)
|
||||
# define DLL_GIVEFNPTRSTODLL CR_LINKAGE_C void CR_STDCALL
|
||||
# elif defined(CR_CXX_CLANG) || defined(CR_CXX_GCC) || defined(CR_ARCH_X64)
|
||||
# define DLL_GIVEFNPTRSTODLL CR_EXPORT void CR_STDCALL
|
||||
# endif
|
||||
#elif defined(CR_LINUX) || defined (CR_OSX) || defined (CR_ANDROID)
|
||||
# define DLL_GIVEFNPTRSTODLL CR_EXPORT void
|
||||
#endif
|
||||
|
||||
DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *functionTable, globalvars_t *glob) {
|
||||
// this is the very first function that is called in the game DLL by the game. Its purpose
|
||||
// is to set the functions interfacing up, by exchanging the functionTable function list
|
||||
// along with a pointer to the engine's global variables structure pGlobals, with the game
|
||||
// DLL. We can there decide if we want to load the normal game DLL just behind our bot DLL,
|
||||
// or any other game DLL that is present, such as Will Day's metamod. Also, since there is
|
||||
// a known bug on Win32 platforms that prevent hook DLLs (such as our bot DLL) to be used in
|
||||
// single player games (because they don't export all the stuff they should), we may need to
|
||||
// build our own array of exported symbols from the actual game DLL in order to use it as
|
||||
// such if necessary. Nothing really bot-related is done in this function. The actual bot
|
||||
// initialization stuff will be done later, when we'll be certain to have a multilayer game.
|
||||
|
||||
// get the engine functions from the game...
|
||||
memcpy (&engfuncs, functionTable, sizeof (enginefuncs_t));
|
||||
globals = glob;
|
||||
|
||||
if (game.postload ()) {
|
||||
return;
|
||||
}
|
||||
auto api_GiveFnptrsToDll = game.lib ().resolve <decltype (&GiveFnptrsToDll)> (__FUNCTION__);
|
||||
|
||||
if (!api_GiveFnptrsToDll) {
|
||||
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__);
|
||||
}
|
||||
GetEngineFunctions (functionTable, nullptr);
|
||||
|
||||
// initialize dynamic linkents
|
||||
ents.initialize ();
|
||||
|
||||
// give the engine functions to the other DLL...
|
||||
api_GiveFnptrsToDll (functionTable, glob);
|
||||
}
|
||||
|
||||
// add linkents for android
|
||||
#include "android.cpp"
|
||||
2434
src/manager.cpp
Normal file
2434
src/manager.cpp
Normal file
File diff suppressed because it is too large
Load diff
531
src/message.cpp
Normal file
531
src/message.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2020 YaPB Development Team <team@yapb.ru>.
|
||||
//
|
||||
// This program 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
void MessageDispatcher::netMsgTextMsg () {
|
||||
enum args { msg = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min) {
|
||||
return;
|
||||
}
|
||||
|
||||
// lookup cached message
|
||||
auto cached = m_textMsgCache[m_args[msg].chars_];
|
||||
|
||||
// check if we're need to handle message
|
||||
if (!(cached & TextMsgCache::NeedHandle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset bomb position for all the bots
|
||||
const auto resetBombPosition = [] () -> void {
|
||||
if (game.mapIs (MapFlags::Demolition)) {
|
||||
graph.setBombOrigin (true);
|
||||
}
|
||||
};
|
||||
|
||||
if (cached & TextMsgCache::Commencing) {
|
||||
util.setNeedForWelcome (true);
|
||||
}
|
||||
else if (cached & TextMsgCache::CounterWin) {
|
||||
bots.setLastWinner (Team::CT); // update last winner for economics
|
||||
resetBombPosition ();
|
||||
}
|
||||
else if (cached & TextMsgCache::RestartRound) {
|
||||
bots.updateTeamEconomics (Team::CT, true);
|
||||
bots.updateTeamEconomics (Team::Terrorist, true);
|
||||
|
||||
extern ConVar mp_startmoney;
|
||||
|
||||
// set balance for all players
|
||||
bots.forEach ([] (Bot *bot) {
|
||||
bot->m_moneyAmount = mp_startmoney.int_ ();
|
||||
return false;
|
||||
});
|
||||
|
||||
resetBombPosition ();
|
||||
}
|
||||
else if (cached & TextMsgCache::TerroristWin) {
|
||||
bots.setLastWinner (Team::Terrorist); // update last winner for economics
|
||||
resetBombPosition ();
|
||||
}
|
||||
else if ((cached & TextMsgCache::BombPlanted) && !bots.isBombPlanted ()) {
|
||||
bots.setBombPlanted (true);
|
||||
|
||||
for (const auto ¬ify : bots) {
|
||||
if (notify->m_notKilled) {
|
||||
notify->clearSearchNodes ();
|
||||
|
||||
// clear only camp tasks
|
||||
notify->clearTask (Task::Camp);
|
||||
|
||||
if (cv_radio_mode.int_ () == 2 && rg.chance (55) && notify->m_team == Team::CT) {
|
||||
notify->pushChatterMessage (Chatter::WhereIsTheC4);
|
||||
}
|
||||
}
|
||||
}
|
||||
graph.setBombOrigin ();
|
||||
}
|
||||
|
||||
// check for burst fire message
|
||||
if (m_bot) {
|
||||
if (cached & TextMsgCache::BurstOn) {
|
||||
m_bot->m_weaponBurstMode = BurstMode::On;
|
||||
}
|
||||
else if (cached & TextMsgCache::BurstOff) {
|
||||
m_bot->m_weaponBurstMode = BurstMode::Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgVGUIMenu () {
|
||||
// this message is sent when a VGUI menu is displayed.
|
||||
|
||||
enum args { menu = 0, min = 1 };
|
||||
|
||||
// check the minimum states or existance of bot
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (m_args[menu].long_) {
|
||||
case GuiMenu::TeamSelect:
|
||||
m_bot->m_startAction = BotMsg::TeamSelect;
|
||||
break;
|
||||
|
||||
case GuiMenu::TerroristSelect:
|
||||
case GuiMenu::CTSelect:
|
||||
m_bot->m_startAction = BotMsg::ClassSelect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgShowMenu () {
|
||||
// this message is sent when a text menu is displayed.
|
||||
|
||||
enum args { menu = 3, min = 4 };
|
||||
|
||||
// check the minimum states or existance of bot
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
auto cached = m_showMenuCache[m_args[menu].chars_];
|
||||
|
||||
// only assign if non-zero
|
||||
if (cached > 0) {
|
||||
m_bot->m_startAction = cached;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgWeaponList () {
|
||||
// this message is sent when a client joins the game. All of the weapons are sent with the weapon ID and information about what ammo is used.
|
||||
|
||||
enum args { classname = 0, ammo_index_1 = 1, max_ammo_1 = 2, slot = 5, slot_pos = 6, id = 7, flags = 8, min = 9 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min) {
|
||||
return;
|
||||
}
|
||||
|
||||
// store away this weapon with it's ammo information...
|
||||
conf.getWeaponProp (m_args[id].long_) = {
|
||||
m_args[classname].chars_,
|
||||
m_args[ammo_index_1].long_,
|
||||
m_args[max_ammo_1].long_,
|
||||
m_args[slot].long_,
|
||||
m_args[slot_pos].long_,
|
||||
m_args[id].long_,
|
||||
m_args[flags].long_
|
||||
};
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgCurWeapon () {
|
||||
// this message is sent when a weapon is selected (either by the bot chosing a weapon or by the server auto assigning the bot a weapon). In CS it's also called when Ammo is increased/decreased
|
||||
|
||||
enum args { state = 0, id = 1, clip = 2, min = 3 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_args[id].long_ < kMaxWeapons) {
|
||||
if (m_args[state].long_ != 0) {
|
||||
m_bot->m_currentWeapon = m_args[id].long_;
|
||||
}
|
||||
|
||||
// ammo amount decreased ? must have fired a bullet...
|
||||
if (m_args[id].long_ == m_bot->m_currentWeapon && m_bot->m_ammoInClip[m_args[id].long_] > m_args[clip].long_) {
|
||||
m_bot->m_timeLastFired = game.time (); // remember the last bullet time
|
||||
}
|
||||
m_bot->m_ammoInClip[m_args[id].long_] = m_args[clip].long_;
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgAmmoX () {
|
||||
// this message is sent whenever ammo amounts are adjusted (up or down). NOTE: Logging reveals that CS uses it very unreliable!
|
||||
|
||||
#if 1
|
||||
netMsgAmmoPickup ();
|
||||
#else
|
||||
enum args { index = 0, value = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
m_bot->m_ammo[m_args[index].long_] = m_args[value].long_; // store it away
|
||||
#endif
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgAmmoPickup () {
|
||||
// this message is sent when the bot picks up some ammo (AmmoX messages are also sent so this message is probably
|
||||
// not really necessary except it allows the HUD to draw pictures of ammo that have been picked up. The bots
|
||||
// don't really need pictures since they don't have any eyes anyway.
|
||||
|
||||
enum args { index = 0, value = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
m_bot->m_ammo[m_args[index].long_] = m_args[value].long_; // store it away
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgDamage () {
|
||||
// this message gets sent when the bots are getting damaged.
|
||||
|
||||
enum args { armor = 0, health = 1, bits = 2, min = 3 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle damage if any
|
||||
if (m_args[armor].long_ > 0 || m_args[health].long_) {
|
||||
m_bot->takeDamage (m_bot->pev->dmg_inflictor, m_args[health].long_, m_args[armor].long_, m_args[bits].long_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgMoney () {
|
||||
// this message gets sent when the bots money amount changes
|
||||
|
||||
enum args { money = 0, min = 1 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
m_bot->m_moneyAmount = m_args[money].long_;
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgStatusIcon () {
|
||||
enum args { enabled = 0, icon = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
// lookup cached icon
|
||||
auto cached = m_statusIconCache[m_args[icon].chars_];
|
||||
|
||||
// check if we're need to handle message
|
||||
if (!(cached & TextMsgCache::NeedHandle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle cases
|
||||
if (cached & StatusIconCache::BuyZone) {
|
||||
m_bot->m_inBuyZone = (m_args[enabled].long_ != 0);
|
||||
|
||||
// try to equip in buyzone
|
||||
m_bot->enteredBuyZone (BuyState::PrimaryWeapon);
|
||||
}
|
||||
else if (cached & StatusIconCache::VipSafety) {
|
||||
m_bot->m_inVIPZone = (m_args[enabled].long_ != 0);
|
||||
}
|
||||
else if (cached & StatusIconCache::C4) {
|
||||
m_bot->m_inBombZone = (m_args[enabled].long_ == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgDeathMsg () {
|
||||
// this message gets sent when player kills player
|
||||
|
||||
enum args { killer = 0, victim = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto killerEntity = game.entityOfIndex (m_args[killer].long_);
|
||||
auto victimEntity = game.entityOfIndex (m_args[victim].long_);
|
||||
|
||||
if (game.isNullEntity (killerEntity) || game.isNullEntity (victimEntity) || victimEntity == killerEntity) {
|
||||
return;
|
||||
}
|
||||
bots.handleDeath (killerEntity, victimEntity);
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgScreenFade () {
|
||||
// this message gets sent when the screen fades (flashbang)
|
||||
|
||||
enum args { r = 3, g = 4, b = 5, alpha = 6, min = 7 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// screen completely faded ?
|
||||
if (m_args[r].long_ >= 255 && m_args[g].long_ >= 255 && m_args[b].long_ >= 255 && m_args[alpha].long_ > 170) {
|
||||
m_bot->takeBlind (m_args[alpha].long_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgHLTV () {
|
||||
// this message gets sent when new round is started in modern cs versions
|
||||
|
||||
enum args { players = 0, fov = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min) {
|
||||
return;
|
||||
}
|
||||
|
||||
// need to start new round ? (we're tracking FOV reset message)
|
||||
if (m_args[players].long_ == 0 && m_args[fov].long_ == 0) {
|
||||
bots.initRound ();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgTeamInfo () {
|
||||
// this message gets sent when player team index is changed
|
||||
|
||||
enum args { index = 0, team = 1, min = 2 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min) {
|
||||
return;
|
||||
}
|
||||
auto &client = util.getClient (m_args[index].long_ - 1);
|
||||
|
||||
// update player team
|
||||
client.team2 = m_teamInfoCache[m_args[team].chars_]; // update real team
|
||||
client.team = game.is (GameFlags::FreeForAll) ? m_args[index].long_ : client.team2;
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgBarTime () {
|
||||
enum args { enabled = 0, min = 1 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if has progress bar
|
||||
if (m_args[enabled].long_ > 0) {
|
||||
m_bot->m_hasProgressBar = true; // the progress bar on a hud
|
||||
|
||||
// notify bots about defusing has started
|
||||
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_bot->m_team == Team::CT) {
|
||||
bots.notifyBombDefuse ();
|
||||
}
|
||||
}
|
||||
else {
|
||||
m_bot->m_hasProgressBar = false; // no progress bar or disappeared
|
||||
}
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgItemStatus () {
|
||||
enum args { value = 0, min = 1 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
auto mask = m_args[value].long_;
|
||||
|
||||
m_bot->m_hasNVG = !!(mask & ItemStatus::Nightvision);
|
||||
m_bot->m_hasDefuser = !!(mask & ItemStatus::DefusalKit);
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgNVGToggle () {
|
||||
enum args { value = 0, min = 1 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
m_bot->m_usesNVG = m_args[value].long_ > 0;
|
||||
}
|
||||
|
||||
void MessageDispatcher::netMsgFlashBat () {
|
||||
enum args { value = 0, min = 1 };
|
||||
|
||||
// check the minimum states
|
||||
if (m_args.length () < min || !m_bot) {
|
||||
return;
|
||||
}
|
||||
m_bot->m_flashLevel = m_args[value].float_;
|
||||
}
|
||||
|
||||
MessageDispatcher::MessageDispatcher () {
|
||||
|
||||
// register wanted message
|
||||
auto pushWanted = [&] (StringRef name, NetMsg id, MsgFunc handler) -> void {
|
||||
m_wanted[name] = id;
|
||||
m_handlers[id] = handler;
|
||||
};
|
||||
reset ();
|
||||
|
||||
// we want to handle next messages
|
||||
pushWanted ("TextMsg", NetMsg::TextMsg, &MessageDispatcher::netMsgTextMsg);
|
||||
pushWanted ("VGUIMenu", NetMsg::VGUIMenu, &MessageDispatcher::netMsgVGUIMenu);
|
||||
pushWanted ("ShowMenu", NetMsg::ShowMenu, &MessageDispatcher::netMsgShowMenu);
|
||||
pushWanted ("WeaponList", NetMsg::WeaponList, &MessageDispatcher::netMsgWeaponList);
|
||||
pushWanted ("CurWeapon", NetMsg::CurWeapon, &MessageDispatcher::netMsgCurWeapon);
|
||||
pushWanted ("AmmoX", NetMsg::AmmoX, &MessageDispatcher::netMsgAmmoX);
|
||||
pushWanted ("AmmoPickup", NetMsg::AmmoPickup, &MessageDispatcher::netMsgAmmoPickup);
|
||||
pushWanted ("Damage", NetMsg::Damage, &MessageDispatcher::netMsgDamage);
|
||||
pushWanted ("Money", NetMsg::Money, &MessageDispatcher::netMsgMoney);
|
||||
pushWanted ("StatusIcon", NetMsg::StatusIcon, &MessageDispatcher::netMsgStatusIcon);
|
||||
pushWanted ("DeathMsg", NetMsg::DeathMsg, &MessageDispatcher::netMsgDeathMsg);
|
||||
pushWanted ("ScreenFade", NetMsg::ScreenFade, &MessageDispatcher::netMsgScreenFade);
|
||||
pushWanted ("HLTV", NetMsg::HLTV, &MessageDispatcher::netMsgHLTV);
|
||||
pushWanted ("TeamInfo", NetMsg::TeamInfo, &MessageDispatcher::netMsgTeamInfo);
|
||||
pushWanted ("BarTime", NetMsg::BarTime, &MessageDispatcher::netMsgBarTime);
|
||||
pushWanted ("ItemStatus", NetMsg::ItemStatus, &MessageDispatcher::netMsgItemStatus);
|
||||
pushWanted ("NVGToggle", NetMsg::NVGToggle, &MessageDispatcher::netMsgNVGToggle);
|
||||
pushWanted ("FlashBat", NetMsg::FlashBat, &MessageDispatcher::netMsgFlashBat);
|
||||
|
||||
// we're need next messages IDs but we're won't handle them, so they will be removed from wanted list as soon as they get engine IDs
|
||||
pushWanted ("BotVoice", NetMsg::BotVoice, nullptr);
|
||||
pushWanted ("SendAudio", NetMsg::SendAudio, nullptr);
|
||||
|
||||
// register text msg cache
|
||||
m_textMsgCache["#CTs_Win"] = TextMsgCache::NeedHandle | TextMsgCache::CounterWin;
|
||||
m_textMsgCache["#Bomb_Defused"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Bomb_Planted"] = TextMsgCache::NeedHandle | TextMsgCache::BombPlanted;
|
||||
m_textMsgCache["#Terrorists_Win"] = TextMsgCache::NeedHandle | TextMsgCache::TerroristWin;
|
||||
m_textMsgCache["#Round_Draw"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#All_Hostages_Rescued"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Target_Saved"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Hostages_Not_Rescued"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Terrorists_Not_Escaped"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#VIP_Not_Escaped"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Escaping_Terrorists_Neutralized"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#VIP_Assassinated"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#VIP_Escaped"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Terrorists_Escaped"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#CTs_PreventEscape"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Target_Bombed"] = TextMsgCache::NeedHandle;
|
||||
m_textMsgCache["#Game_Commencing"] = TextMsgCache::NeedHandle | TextMsgCache::Commencing;
|
||||
m_textMsgCache["#Game_will_restart_in"] = TextMsgCache::NeedHandle | TextMsgCache::RestartRound;
|
||||
m_textMsgCache["#Switch_To_BurstFire"] = TextMsgCache::NeedHandle | TextMsgCache::BurstOn;
|
||||
m_textMsgCache["#Switch_To_SemiAuto"] = TextMsgCache::NeedHandle | TextMsgCache::BurstOff;
|
||||
|
||||
// register show menu cache
|
||||
m_showMenuCache["#Team_Select"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#Team_Select_Spect"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#IG_Team_Select_Spect"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#IG_Team_Select"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#IG_VIP_Team_Select"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#IG_VIP_Team_Select_Spect"] = BotMsg::TeamSelect;
|
||||
m_showMenuCache["#Terrorist_Select"] = BotMsg::ClassSelect;
|
||||
m_showMenuCache["#CT_Select"] = BotMsg::ClassSelect;
|
||||
|
||||
// register status icon cache
|
||||
m_statusIconCache["buyzone"] = StatusIconCache::NeedHandle | StatusIconCache::BuyZone;
|
||||
m_statusIconCache["vipsafety"] = StatusIconCache::NeedHandle | StatusIconCache::VipSafety;
|
||||
m_statusIconCache["c4"] = StatusIconCache::NeedHandle | StatusIconCache::C4;
|
||||
|
||||
// register team info cache
|
||||
m_teamInfoCache["TERRORIST"] = Team::Terrorist;
|
||||
m_teamInfoCache["UNASSIGNED"] = Team::Unassigned;
|
||||
m_teamInfoCache["SPECTATOR"] = Team::Spectator;
|
||||
m_teamInfoCache["CT"] = Team::CT;
|
||||
}
|
||||
|
||||
int32 MessageDispatcher::add (StringRef name, int32 id) {
|
||||
if (!m_wanted.exists (name)) {
|
||||
return id;
|
||||
}
|
||||
m_maps[m_wanted[name]] = id; // add message from engine regusermsg
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void MessageDispatcher::start (edict_t *ent, int32 type) {
|
||||
reset ();
|
||||
|
||||
// search if we need to handle this message
|
||||
for (const auto &msg : m_maps) {
|
||||
if (msg.value == type && m_handlers[msg.key]) {
|
||||
m_current = msg.key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no messagem no processing
|
||||
if (m_current == NetMsg::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
// message for bot bot?
|
||||
if (!game.isNullEntity (ent) && !(ent->v.flags & FL_DORMANT)) {
|
||||
m_bot = bots[ent];
|
||||
|
||||
if (!m_bot) {
|
||||
m_current = NetMsg::None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_args.clear (); // clear previous args
|
||||
}
|
||||
|
||||
void MessageDispatcher::stop () {
|
||||
if (m_current == NetMsg::None) {
|
||||
return;
|
||||
}
|
||||
(this->*m_handlers[m_current]) ();
|
||||
m_current = NetMsg::None;
|
||||
}
|
||||
|
||||
void MessageDispatcher::ensureMessages () {
|
||||
// we're getting messages ids in regusermsg for metamod, but when we're unloaded, we're lost our ids on next 'meta load'.
|
||||
// this function tries to associate appropriate message ids.
|
||||
|
||||
// check if we're have one
|
||||
if (m_maps.exists (NetMsg::Money)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// re-register our message
|
||||
for (const auto &msg : m_wanted) {
|
||||
add (msg.key, GET_USER_MSG_ID (PLID, msg.key.chars (), nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
int32 MessageDispatcher::id (NetMsg msg) {
|
||||
if (game.is (GameFlags::Metamod)) {
|
||||
ensureMessages ();
|
||||
}
|
||||
return m_maps[msg];
|
||||
}
|
||||
3108
src/navigate.cpp
Normal file
3108
src/navigate.cpp
Normal file
File diff suppressed because it is too large
Load diff
725
src/support.cpp
Normal file
725
src/support.cpp
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2020 YaPB Development Team <team@yapb.ru>.
|
||||
//
|
||||
// This program 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_display_welcome_text ("ub_display_welcome_text", "1", "Enables or disables showing welcome message to host entity on game start.");
|
||||
ConVar cv_enable_query_hook ("ub_enable_query_hook", "1", "Enables or disables fake server queries response, that shows bots as real players in server browser.");
|
||||
|
||||
BotSupport::BotSupport () {
|
||||
m_needToSendWelcome = false;
|
||||
m_welcomeReceiveTime = 0.0f;
|
||||
|
||||
// add default messages
|
||||
m_sentences.push ("hello user,communication is acquired");
|
||||
m_sentences.push ("your presence is acknowledged");
|
||||
m_sentences.push ("high man, your in command now");
|
||||
m_sentences.push ("blast your hostile for good");
|
||||
m_sentences.push ("high man, kill some idiot here");
|
||||
m_sentences.push ("is there a doctor in the area");
|
||||
m_sentences.push ("warning, experimental materials detected");
|
||||
m_sentences.push ("high amigo, shoot some but");
|
||||
m_sentences.push ("attention, hours of work software, detected");
|
||||
m_sentences.push ("time for some bad ass explosion");
|
||||
m_sentences.push ("bad ass son of a breach device activated");
|
||||
m_sentences.push ("high, do not question this great service");
|
||||
m_sentences.push ("engine is operative, hello and goodbye");
|
||||
m_sentences.push ("high amigo, your administration has been great last day");
|
||||
m_sentences.push ("attention, expect experimental armed hostile presence");
|
||||
m_sentences.push ("warning, medical attention required");
|
||||
|
||||
m_tags.emplace ("[[", "]]");
|
||||
m_tags.emplace ("-=", "=-");
|
||||
m_tags.emplace ("-[", "]-");
|
||||
m_tags.emplace ("-]", "[-");
|
||||
m_tags.emplace ("-}", "{-");
|
||||
m_tags.emplace ("-{", "}-");
|
||||
m_tags.emplace ("<[", "]>");
|
||||
m_tags.emplace ("<]", "[>");
|
||||
m_tags.emplace ("[-", "-]");
|
||||
m_tags.emplace ("]-", "-[");
|
||||
m_tags.emplace ("{-", "-}");
|
||||
m_tags.emplace ("}-", "-{");
|
||||
m_tags.emplace ("[", "]");
|
||||
m_tags.emplace ("{", "}");
|
||||
m_tags.emplace ("<", "[");
|
||||
m_tags.emplace (">", "<");
|
||||
m_tags.emplace ("-", "-");
|
||||
m_tags.emplace ("|", "|");
|
||||
m_tags.emplace ("=", "=");
|
||||
m_tags.emplace ("+", "+");
|
||||
m_tags.emplace ("(", ")");
|
||||
m_tags.emplace (")", "(");
|
||||
|
||||
// register noise cache
|
||||
m_noiseCache["player/bhit"] = Noise::NeedHandle | Noise::HitFall;
|
||||
m_noiseCache["player/head"] = Noise::NeedHandle | Noise::HitFall;
|
||||
m_noiseCache["items/gunpi"] = Noise::NeedHandle | Noise::Pickup;
|
||||
m_noiseCache["items/9mmcl"] = Noise::NeedHandle | Noise::Ammo;
|
||||
m_noiseCache["weapons/zoo"] = Noise::NeedHandle | Noise::Zoom;
|
||||
m_noiseCache["hostage/hos"] = Noise::NeedHandle | Noise::Hostage;
|
||||
m_noiseCache["debris/bust"] = Noise::NeedHandle | Noise::Broke;
|
||||
m_noiseCache["doors/doorm"] = Noise::NeedHandle | Noise::Door;
|
||||
|
||||
// register weapon aliases
|
||||
m_weaponAlias.push (Weapon::USP, "usp"); // HK USP .45 Tactical
|
||||
m_weaponAlias.push (Weapon::Glock18, "glock"); // Glock18 Select Fire
|
||||
m_weaponAlias.push (Weapon::Deagle, "deagle"); // Desert Eagle .50AE
|
||||
m_weaponAlias.push (Weapon::P228, "p228"); // SIG P228
|
||||
m_weaponAlias.push (Weapon::Elite, "elite"); // Dual Beretta 96G Elite
|
||||
m_weaponAlias.push (Weapon::FiveSeven, "fn57"); // FN Five-Seven
|
||||
m_weaponAlias.push (Weapon::M3, "m3"); // Benelli M3 Super90
|
||||
m_weaponAlias.push (Weapon::XM1014, "xm1014"); // Benelli XM1014
|
||||
m_weaponAlias.push (Weapon::MP5, "mp5"); // HK MP5-Navy
|
||||
m_weaponAlias.push (Weapon::TMP, "tmp"); // Steyr Tactical Machine Pistol
|
||||
m_weaponAlias.push (Weapon::P90, "p90"); // FN P90
|
||||
m_weaponAlias.push (Weapon::MAC10, "mac10"); // Ingram MAC-10
|
||||
m_weaponAlias.push (Weapon::UMP45, "ump45"); // HK UMP45
|
||||
m_weaponAlias.push (Weapon::AK47, "ak47"); // Automat Kalashnikov AK-47
|
||||
m_weaponAlias.push (Weapon::Galil, "galil"); // IMI Galil
|
||||
m_weaponAlias.push (Weapon::Famas, "famas"); // GIAT FAMAS
|
||||
m_weaponAlias.push (Weapon::SG552, "sg552"); // Sig SG-552 Commando
|
||||
m_weaponAlias.push (Weapon::M4A1, "m4a1"); // Colt M4A1 Carbine
|
||||
m_weaponAlias.push (Weapon::AUG, "aug"); // Steyr Aug
|
||||
m_weaponAlias.push (Weapon::Scout, "scout"); // Steyr Scout
|
||||
m_weaponAlias.push (Weapon::AWP, "awp"); // AI Arctic Warfare/Magnum
|
||||
m_weaponAlias.push (Weapon::G3SG1, "g3sg1"); // HK G3/SG-1 Sniper Rifle
|
||||
m_weaponAlias.push (Weapon::SG550, "sg550"); // Sig SG-550 Sniper
|
||||
m_weaponAlias.push (Weapon::M249, "m249"); // FN M249 Para
|
||||
m_weaponAlias.push (Weapon::Flashbang, "flash"); // Concussion Grenade
|
||||
m_weaponAlias.push (Weapon::Explosive, "hegren"); // High-Explosive Grenade
|
||||
m_weaponAlias.push (Weapon::Smoke, "sgren"); // Smoke Grenade
|
||||
m_weaponAlias.push (Weapon::Armor, "vest"); // Kevlar Vest
|
||||
m_weaponAlias.push (Weapon::ArmorHelm, "vesthelm"); // Kevlar Vest and Helmet
|
||||
m_weaponAlias.push (Weapon::Defuser, "defuser"); // Defuser Kit
|
||||
m_weaponAlias.push (Weapon::Shield, "shield"); // Tactical Shield
|
||||
m_weaponAlias.push (Weapon::Knife, "knife"); // Knife
|
||||
|
||||
m_clients.resize (kGameMaxPlayers + 1);
|
||||
}
|
||||
|
||||
bool BotSupport::isAlive (edict_t *ent) {
|
||||
if (game.isNullEntity (ent)) {
|
||||
return false;
|
||||
}
|
||||
return ent->v.deadflag == DEAD_NO && ent->v.health > 0 && ent->v.movetype != MOVETYPE_NOCLIP;
|
||||
}
|
||||
|
||||
bool BotSupport::isVisible (const Vector &origin, edict_t *ent) {
|
||||
if (game.isNullEntity (ent)) {
|
||||
return false;
|
||||
}
|
||||
TraceResult tr {};
|
||||
game.testLine (ent->v.origin + ent->v.view_ofs, origin, TraceIgnore::Everything, ent, &tr);
|
||||
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BotSupport::traceDecals (entvars_t *pev, TraceResult *trace, int logotypeIndex) {
|
||||
// this function draw spraypaint depending on the tracing results.
|
||||
|
||||
auto logo = conf.getRandomLogoName (logotypeIndex);
|
||||
|
||||
int entityIndex = -1, message = TE_DECAL;
|
||||
int decalIndex = engfuncs.pfnDecalIndex (logo.chars ());
|
||||
|
||||
if (decalIndex < 0) {
|
||||
decalIndex = engfuncs.pfnDecalIndex ("{lambda06");
|
||||
}
|
||||
|
||||
if (cr::fequal (trace->flFraction, 1.0f)) {
|
||||
return;
|
||||
}
|
||||
if (!game.isNullEntity (trace->pHit)) {
|
||||
if (trace->pHit->v.solid == SOLID_BSP || trace->pHit->v.movetype == MOVETYPE_PUSHSTEP) {
|
||||
entityIndex = game.indexOfEntity (trace->pHit);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
entityIndex = 0;
|
||||
}
|
||||
|
||||
if (entityIndex != 0) {
|
||||
if (decalIndex > 255) {
|
||||
message = TE_DECALHIGH;
|
||||
decalIndex -= 256;
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = TE_WORLDDECAL;
|
||||
|
||||
if (decalIndex > 255) {
|
||||
message = TE_WORLDDECALHIGH;
|
||||
decalIndex -= 256;
|
||||
}
|
||||
}
|
||||
|
||||
if (logo.startsWith ("{")) {
|
||||
MessageWriter (MSG_BROADCAST, SVC_TEMPENTITY)
|
||||
.writeByte (TE_PLAYERDECAL)
|
||||
.writeByte (game.indexOfEntity (pev->pContainingEntity))
|
||||
.writeCoord (trace->vecEndPos.x)
|
||||
.writeCoord (trace->vecEndPos.y)
|
||||
.writeCoord (trace->vecEndPos.z)
|
||||
.writeShort (static_cast <short> (game.indexOfEntity (trace->pHit)))
|
||||
.writeByte (decalIndex);
|
||||
}
|
||||
else {
|
||||
MessageWriter msg;
|
||||
|
||||
msg.start (MSG_BROADCAST, SVC_TEMPENTITY)
|
||||
.writeByte (message)
|
||||
.writeCoord (trace->vecEndPos.x)
|
||||
.writeCoord (trace->vecEndPos.y)
|
||||
.writeCoord (trace->vecEndPos.z)
|
||||
.writeByte (decalIndex);
|
||||
|
||||
if (entityIndex) {
|
||||
msg.writeShort (entityIndex);
|
||||
}
|
||||
msg.end ();
|
||||
}
|
||||
}
|
||||
|
||||
bool BotSupport::isPlayer (edict_t *ent) {
|
||||
if (game.isNullEntity (ent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ent->v.flags & FL_PROXY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ent->v.flags & (FL_CLIENT | FL_FAKECLIENT)) || bots[ent] != nullptr) {
|
||||
return !strings.isEmpty (ent->v.netname.chars ());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BotSupport::isPlayerVIP (edict_t *ent) {
|
||||
if (!game.mapIs (MapFlags::Assassination)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPlayer (ent)) {
|
||||
return false;
|
||||
}
|
||||
return *(engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "model")) == 'v';
|
||||
}
|
||||
|
||||
bool BotSupport::isFakeClient (edict_t *ent) {
|
||||
if (bots[ent] != nullptr || (!game.isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BotSupport::openConfig (const char *fileName, const char *errorIfNotExists, MemFile *outFile, bool languageDependant /*= false*/) {
|
||||
if (*outFile) {
|
||||
outFile->close ();
|
||||
}
|
||||
|
||||
// save config dir
|
||||
auto configDir = strings.format ("addons/%s/conf", product.folder);
|
||||
|
||||
if (languageDependant) {
|
||||
if (strcmp (fileName, "lang.cfg") == 0 && strcmp (cv_language.str (), "en") == 0) {
|
||||
return false;
|
||||
}
|
||||
auto langConfig = strings.format ("%s/lang/%s_%s", configDir, cv_language.str (), fileName);
|
||||
|
||||
// check is file is exists for this language
|
||||
if (!outFile->open (langConfig)) {
|
||||
outFile->open (strings.format ("%s/lang/en_%s", configDir, fileName));
|
||||
}
|
||||
}
|
||||
else {
|
||||
outFile->open (strings.format ("%s/%s", configDir, fileName));
|
||||
}
|
||||
|
||||
if (!*outFile) {
|
||||
logger.error (errorIfNotExists);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BotSupport::checkWelcome () {
|
||||
// the purpose of this function, is to send quick welcome message, to the listenserver entity.
|
||||
|
||||
if (game.isDedicated () || !cv_display_welcome_text.bool_ () || !m_needToSendWelcome) {
|
||||
return;
|
||||
}
|
||||
m_welcomeReceiveTime = 0.0f;
|
||||
|
||||
|
||||
bool needToSendMsg = (graph.length () > 0 ? m_needToSendWelcome : true);
|
||||
auto receiveEntity = game.getLocalEntity ();
|
||||
|
||||
if (isAlive (receiveEntity) && m_welcomeReceiveTime < 1.0 && needToSendMsg) {
|
||||
m_welcomeReceiveTime = game.time () + 4.0f; // receive welcome message in four seconds after game has commencing
|
||||
}
|
||||
|
||||
if (m_welcomeReceiveTime > 0.0f && needToSendMsg) {
|
||||
if (!game.is (GameFlags::Mobility | GameFlags::Xash3D)) {
|
||||
game.serverCommand ("speak \"%s\"", m_sentences.random ());
|
||||
}
|
||||
|
||||
MessageWriter (MSG_ONE, msgs.id (NetMsg::TextMsg), nullptr, receiveEntity)
|
||||
.writeByte (HUD_PRINTTALK)
|
||||
.writeString (strings.format ("----- %s v%s (Build: %s), {%s}, (c) %s, by %s (%s)-----", product.name, product.version, product.build.count, product.date, product.year, product.author, product.url));
|
||||
|
||||
MessageWriter (MSG_ONE, SVC_TEMPENTITY, nullptr, receiveEntity)
|
||||
.writeByte (TE_TEXTMESSAGE)
|
||||
.writeByte (1)
|
||||
.writeShort (MessageWriter::fs16 (-1.0f, 13.0f))
|
||||
.writeShort (MessageWriter::fs16 (-1.0f, 13.0f))
|
||||
.writeByte (2)
|
||||
.writeByte (rg.int_ (33, 255))
|
||||
.writeByte (rg.int_ (33, 255))
|
||||
.writeByte (rg.int_ (33, 255))
|
||||
.writeByte (0)
|
||||
.writeByte (rg.int_ (230, 255))
|
||||
.writeByte (rg.int_ (230, 255))
|
||||
.writeByte (rg.int_ (230, 255))
|
||||
.writeByte (200)
|
||||
.writeShort (MessageWriter::fu16 (0.0078125f, 8.0f))
|
||||
.writeShort (MessageWriter::fu16 (2.0f, 8.0f))
|
||||
.writeShort (MessageWriter::fu16 (6.0f, 8.0f))
|
||||
.writeShort (MessageWriter::fu16 (0.1f, 8.0f))
|
||||
.writeString (strings.format ("\nHello! You are playing with %s v%s (Build: %s)\nDevised by %s\n\n%s", product.name, product.version, product.build.count, product.author, graph.getAuthor ()));
|
||||
|
||||
m_welcomeReceiveTime = 0.0f;
|
||||
m_needToSendWelcome = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool BotSupport::findNearestPlayer (void **pvHolder, edict_t *to, float searchDistance, bool sameTeam, bool needBot, bool needAlive, bool needDrawn, bool needBotWithC4) {
|
||||
// this function finds nearest to to, player with set of parameters, like his
|
||||
// team, live status, search distance etc. if needBot is true, then pvHolder, will
|
||||
// be filled with bot pointer, else with edict pointer(!).
|
||||
|
||||
edict_t *survive = nullptr; // pointer to temporally & survive entity
|
||||
float nearestPlayer = 4096.0f; // nearest player
|
||||
|
||||
int toTeam = game.getTeam (to);
|
||||
|
||||
for (const auto &client : m_clients) {
|
||||
if (!(client.flags & ClientFlags::Used) || client.ent == to) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((sameTeam && client.team != toTeam) || (needAlive && !(client.flags & ClientFlags::Alive)) || (needBot && !isFakeClient (client.ent)) || (needDrawn && (client.ent->v.effects & EF_NODRAW)) || (needBotWithC4 && (client.ent->v.weapons & Weapon::C4))) {
|
||||
continue; // filter players with parameters
|
||||
}
|
||||
float distance = (client.ent->v.origin - to->v.origin).length ();
|
||||
|
||||
if (distance < nearestPlayer && distance < searchDistance) {
|
||||
nearestPlayer = distance;
|
||||
survive = client.ent;
|
||||
}
|
||||
}
|
||||
|
||||
if (game.isNullEntity (survive)) {
|
||||
return false; // nothing found
|
||||
}
|
||||
|
||||
// fill the holder
|
||||
if (needBot) {
|
||||
*pvHolder = reinterpret_cast <void *> (bots[survive]);
|
||||
}
|
||||
else {
|
||||
*pvHolder = reinterpret_cast <void *> (survive);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BotSupport::listenNoise (edict_t *ent, StringRef sample, float volume) {
|
||||
// this function called by the sound hooking code (in emit_sound) enters the played sound into the array associated with the entity
|
||||
|
||||
if (game.isNullEntity (ent) || sample.empty ()) {
|
||||
return;
|
||||
}
|
||||
const Vector &origin = game.getEntityWorldOrigin (ent);
|
||||
|
||||
// something wrong with sound...
|
||||
if (origin.empty ()) {
|
||||
return;
|
||||
}
|
||||
auto noise = m_noiseCache[sample.substr (0, 11)];
|
||||
|
||||
// we're not handling theese
|
||||
if (!(noise & Noise::NeedHandle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find nearest player to sound origin
|
||||
auto findNearbyClient = [&origin] () {
|
||||
float nearest = kInfiniteDistance;
|
||||
Client *result = nullptr;
|
||||
|
||||
// loop through all players
|
||||
for (auto &client : util.getClients ()) {
|
||||
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive)) {
|
||||
continue;
|
||||
}
|
||||
auto distance = (client.origin - origin).lengthSq ();
|
||||
|
||||
// now find nearest player
|
||||
if (distance < nearest) {
|
||||
result = &client;
|
||||
nearest = distance;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
auto client = findNearbyClient ();
|
||||
|
||||
// update noise stats
|
||||
auto registerNoise = [&origin, &client, &volume] (float distance, float lasting) {
|
||||
client->noise.dist = distance * volume;
|
||||
client->noise.last = game.time () + lasting;
|
||||
client->noise.pos = origin;
|
||||
};
|
||||
|
||||
// client wasn't found
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hit/fall sound?
|
||||
if (noise & Noise::HitFall) {
|
||||
registerNoise (768.0f, 0.52f);
|
||||
}
|
||||
|
||||
// weapon pickup?
|
||||
else if (noise & Noise::Pickup) {
|
||||
registerNoise (768.0f, 0.45f);
|
||||
}
|
||||
|
||||
// sniper zooming?
|
||||
else if (noise & Noise::Zoom) {
|
||||
registerNoise (512.0f, 0.10f);
|
||||
}
|
||||
|
||||
// ammo pickup?
|
||||
else if (noise & Noise::Ammo) {
|
||||
registerNoise (512.0f, 0.25f);
|
||||
}
|
||||
|
||||
// ct used hostage?
|
||||
else if (noise & Noise::Hostage) {
|
||||
registerNoise (1024.0f, 5.00);
|
||||
}
|
||||
|
||||
// broke something?
|
||||
else if (noise & Noise::Broke) {
|
||||
registerNoise (1024.0f, 2.00f);
|
||||
}
|
||||
|
||||
// someone opened a door
|
||||
else if (noise & Noise::Door) {
|
||||
registerNoise (1024.0f, 3.00f);
|
||||
}
|
||||
}
|
||||
|
||||
void BotSupport::simulateNoise (int playerIndex) {
|
||||
// this function tries to simulate playing of sounds to let the bots hear sounds which aren't
|
||||
// captured through server sound hooking
|
||||
|
||||
if (playerIndex < 0 || playerIndex >= game.maxClients ()) {
|
||||
return; // reliability check
|
||||
}
|
||||
Client &client = m_clients[playerIndex];
|
||||
ClientNoise noise {};
|
||||
|
||||
auto buttons = client.ent->v.button | client.ent->v.oldbuttons;
|
||||
|
||||
// pressed attack button?
|
||||
if (buttons & IN_ATTACK) {
|
||||
noise.dist = 2048.0f;
|
||||
noise.last = game.time () + 0.3f;
|
||||
}
|
||||
|
||||
// pressed used button?
|
||||
else if (buttons & IN_USE) {
|
||||
noise.dist = 512.0f;
|
||||
noise.last = game.time () + 0.5f;
|
||||
}
|
||||
|
||||
// pressed reload button?
|
||||
else if (buttons & IN_RELOAD) {
|
||||
noise.dist = 512.0f;
|
||||
noise.last = game.time () + 0.5f;
|
||||
}
|
||||
|
||||
// uses ladder?
|
||||
else if (client.ent->v.movetype == MOVETYPE_FLY) {
|
||||
if (cr::abs (client.ent->v.velocity.z) > 50.0f) {
|
||||
noise.dist = 1024.0f;
|
||||
noise.last = game.time () + 0.3f;
|
||||
}
|
||||
}
|
||||
else {
|
||||
extern ConVar mp_footsteps;
|
||||
|
||||
if (mp_footsteps.bool_ ()) {
|
||||
// moves fast enough?
|
||||
noise.dist = 1280.0f * (client.ent->v.velocity.length2d () / 260.0f);
|
||||
noise.last = game.time () + 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
if (noise.dist <= 0.0) {
|
||||
return; // didn't issue sound?
|
||||
}
|
||||
|
||||
// some sound already associated
|
||||
if (client.noise.last > game.time ()) {
|
||||
if (client.noise.dist <= noise.dist) {
|
||||
// override it with new
|
||||
client.noise.dist = noise.dist;
|
||||
client.noise.last = noise.last;
|
||||
client.noise.pos = client.ent->v.origin;
|
||||
}
|
||||
}
|
||||
else if (!cr::fzero (noise.last)) {
|
||||
// just remember it
|
||||
client.noise.dist = noise.dist;
|
||||
client.noise.last = noise.last;
|
||||
client.noise.pos = client.ent->v.origin;
|
||||
}
|
||||
}
|
||||
|
||||
void BotSupport::updateClients () {
|
||||
|
||||
// record some stats of all players on the server
|
||||
for (int i = 0; i < game.maxClients (); ++i) {
|
||||
edict_t *player = game.playerOfIndex (i);
|
||||
Client &client = m_clients[i];
|
||||
|
||||
if (!game.isNullEntity (player) && (player->v.flags & FL_CLIENT)) {
|
||||
client.ent = player;
|
||||
client.flags |= ClientFlags::Used;
|
||||
|
||||
if (util.isAlive (player)) {
|
||||
client.flags |= ClientFlags::Alive;
|
||||
}
|
||||
else {
|
||||
client.flags &= ~ClientFlags::Alive;
|
||||
}
|
||||
|
||||
if (client.flags & ClientFlags::Alive) {
|
||||
client.origin = player->v.origin;
|
||||
simulateNoise (i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
client.flags &= ~(ClientFlags::Used | ClientFlags::Alive);
|
||||
client.ent = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int BotSupport::getPingBitmask (edict_t *ent, int loss, int ping) {
|
||||
// this function generats bitmask for SVC_PINGS engine message. See SV_EmitPings from engine for details
|
||||
|
||||
const auto emit = [] (int s0, int s1, int s2) {
|
||||
return (s0 & (cr::bit (s1) - 1)) << s2;
|
||||
};
|
||||
return emit (loss, 7, 18) | emit (ping, 12, 6) | emit (game.indexOfPlayer (ent), 5, 1) | 1;
|
||||
}
|
||||
|
||||
void BotSupport::calculatePings () {
|
||||
if (!game.is (GameFlags::HasFakePings) || cv_show_latency.int_ () != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Twin <int, int> average { 0, 0 };
|
||||
int numHumans = 0;
|
||||
|
||||
// first get average ping on server, and store real client pings
|
||||
for (auto &client : m_clients) {
|
||||
if (!(client.flags & ClientFlags::Used) || isFakeClient (client.ent)) {
|
||||
continue;
|
||||
}
|
||||
int ping, loss;
|
||||
engfuncs.pfnGetPlayerStats (client.ent, &ping, &loss);
|
||||
|
||||
// store normal client ping
|
||||
client.ping = getPingBitmask (client.ent, loss, ping > 0 ? ping / 2 : rg.int_ (8, 16)); // getting player ping sometimes fails
|
||||
client.pingUpdate = true; // force resend ping
|
||||
|
||||
++numHumans;
|
||||
|
||||
average.first += ping;
|
||||
average.second += loss;
|
||||
}
|
||||
|
||||
if (numHumans > 0) {
|
||||
average.first /= numHumans;
|
||||
average.second /= numHumans;
|
||||
}
|
||||
else {
|
||||
average.first = rg.int_ (30, 40);
|
||||
average.second = rg.int_ (5, 10);
|
||||
}
|
||||
|
||||
// now calculate bot ping based on average from players
|
||||
for (auto &client : m_clients) {
|
||||
if (!(client.flags & ClientFlags::Used)) {
|
||||
continue;
|
||||
}
|
||||
auto bot = bots[client.ent];
|
||||
|
||||
// we're only intrested in bots here
|
||||
if (!bot) {
|
||||
continue;
|
||||
}
|
||||
int part = static_cast <int> (average.first * 0.2f);
|
||||
|
||||
int botPing = bot->m_basePing + rg.int_ (average.first - part, average.first + part) + rg.int_ (bot->m_difficulty / 2, bot->m_difficulty);
|
||||
int botLoss = rg.int_ (average.second / 2, average.second);
|
||||
|
||||
client.ping = getPingBitmask (client.ent, botLoss, botPing);
|
||||
client.pingUpdate = true; // force resend ping
|
||||
}
|
||||
}
|
||||
|
||||
void BotSupport::sendPings (edict_t *to) {
|
||||
MessageWriter msg;
|
||||
|
||||
// missing from sdk
|
||||
constexpr int kGamePingSVC = 17;
|
||||
|
||||
for (auto &client : m_clients) {
|
||||
if (!(client.flags & ClientFlags::Used) || client.ent == game.getLocalEntity ()) {
|
||||
continue;
|
||||
}
|
||||
if (!client.pingUpdate) {
|
||||
continue;
|
||||
}
|
||||
client.pingUpdate = false;
|
||||
|
||||
// no ping, no fun
|
||||
if (!client.ping) {
|
||||
client.ping = getPingBitmask (client.ent, rg.int_ (5, 10), rg.int_ (15, 40));
|
||||
}
|
||||
|
||||
msg.start (MSG_ONE_UNRELIABLE, kGamePingSVC, nullptr, to)
|
||||
.writeLong (client.ping)
|
||||
.end ();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void BotSupport::installSendTo () {
|
||||
// if previously requested to disable?
|
||||
if (!cv_enable_query_hook.bool_ ()) {
|
||||
if (m_sendToHook.enabled ()) {
|
||||
disableSendTo ();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// do not enable on not dedicated server
|
||||
if (!game.isDedicated ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// enable only on modern games
|
||||
if (game.is (GameFlags::Modern) && (plat.linux || plat.win32) && !plat.arm && !m_sendToHook.enabled ()) {
|
||||
m_sendToHook.patch (reinterpret_cast <void *> (&sendto), reinterpret_cast <void *> (&BotSupport::sendTo));
|
||||
}
|
||||
}
|
||||
|
||||
bool BotSupport::isObjectInsidePlane (FrustumPlane &plane, const Vector ¢er, float height, float radius) {
|
||||
auto isPointInsidePlane = [&](const Vector &point) -> bool {
|
||||
return plane.result + (plane.normal | point) >= 0.0f;
|
||||
};
|
||||
|
||||
const Vector &test = plane.normal.get2d ();
|
||||
const Vector &top = center + Vector (0.0f, 0.0f, height * 0.5f) + test * radius;
|
||||
const Vector &bottom = center - Vector (0.0f, 0.0f, height * 0.5f) + test * radius;
|
||||
|
||||
return isPointInsidePlane (top) || isPointInsidePlane (bottom);
|
||||
}
|
||||
|
||||
int32 BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) {
|
||||
const auto send = [&] (const Twin <const uint8 *, size_t> &msg) -> int32 {
|
||||
return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength);
|
||||
};
|
||||
|
||||
auto packet = reinterpret_cast <const uint8 *> (message);
|
||||
|
||||
// player replies response
|
||||
if (length > 5 && packet[0] == 0xff && packet[1] == 0xff && packet[2] == 0xff && packet[3] == 0xff) {
|
||||
|
||||
if (packet[4] == 'D') {
|
||||
QueryBuffer buffer (packet, length, 5);
|
||||
auto count = buffer.read <uint8> ();
|
||||
|
||||
for (uint8 i = 0; i < count; ++i) {
|
||||
buffer.skip <uint8> (); // number
|
||||
buffer.skipString (); // name
|
||||
buffer.skip <int32> (); // score
|
||||
|
||||
auto ctime = buffer.read <float> (); // override connection time
|
||||
buffer.write <float> (bots.getConnectTime (i, ctime));
|
||||
}
|
||||
return send (buffer.data ());
|
||||
}
|
||||
else if (packet[4] == 'I') {
|
||||
QueryBuffer buffer (packet, length, 5);
|
||||
buffer.skip <uint8> (); // protocol
|
||||
|
||||
// skip server name, folder, map game
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
buffer.skipString ();
|
||||
}
|
||||
buffer.skip <short> (); // steam app id
|
||||
buffer.skip <uint8> (); // players
|
||||
buffer.skip <uint8> (); // maxplayers
|
||||
buffer.skip <uint8> (); // bots
|
||||
buffer.write <uint8> (0); // zero out bot count
|
||||
|
||||
return send (buffer.data ());
|
||||
}
|
||||
else if (packet[4] == 'm') {
|
||||
QueryBuffer buffer (packet, length, 5);
|
||||
|
||||
buffer.shiftToEnd (); // shift to the end of buffer
|
||||
buffer.write <uint8> (0); // zero out bot count
|
||||
|
||||
return send (buffer.data ());
|
||||
}
|
||||
}
|
||||
return send ({ packet, length });
|
||||
}
|
||||
|
||||
StringRef BotSupport::weaponIdToAlias (int32 id) {
|
||||
StringRef none = "none";
|
||||
|
||||
if (m_weaponAlias.exists (id)) {
|
||||
return m_weaponAlias[id];
|
||||
}
|
||||
return none;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue