// // YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. // Copyright © 2004-2020 YaPB Development Team . // // 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 ConVar cv_display_welcome_text ("yb_display_welcome_text", "1", "Enables or disables showing welcome message to host entity on game start."); ConVar cv_enable_query_hook ("yb_enable_query_hook", "0", "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 ("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[Weapon::USP] = "usp"; // HK USP .45 Tactical m_weaponAlias[Weapon::Glock18] = "glock"; // Glock18 Select Fire m_weaponAlias[Weapon::Deagle] = "deagle"; // Desert Eagle .50AE m_weaponAlias[Weapon::P228] = "p228"; // SIG P228 m_weaponAlias[Weapon::Elite] = "elite"; // Dual Beretta 96G Elite m_weaponAlias[Weapon::FiveSeven] = "fn57"; // FN Five-Seven m_weaponAlias[Weapon::M3] = "m3"; // Benelli M3 Super90 m_weaponAlias[Weapon::XM1014] = "xm1014"; // Benelli XM1014 m_weaponAlias[Weapon::MP5] = "mp5"; // HK MP5-Navy m_weaponAlias[Weapon::TMP] = "tmp"; // Steyr Tactical Machine Pistol m_weaponAlias[Weapon::P90] = "p90"; // FN P90 m_weaponAlias[Weapon::MAC10] = "mac10"; // Ingram MAC-10 m_weaponAlias[Weapon::UMP45] = "ump45"; // HK UMP45 m_weaponAlias[Weapon::AK47] = "ak47"; // Automat Kalashnikov AK-47 m_weaponAlias[Weapon::Galil] = "galil"; // IMI Galil m_weaponAlias[Weapon::Famas] = "famas"; // GIAT FAMAS m_weaponAlias[Weapon::SG552] = "sg552"; // Sig SG-552 Commando m_weaponAlias[Weapon::M4A1] = "m4a1"; // Colt M4A1 Carbine m_weaponAlias[Weapon::AUG] = "aug"; // Steyr Aug m_weaponAlias[Weapon::Scout] = "scout"; // Steyr Scout m_weaponAlias[Weapon::AWP] = "awp"; // AI Arctic Warfare/Magnum m_weaponAlias[Weapon::G3SG1] = "g3sg1"; // HK G3/SG-1 Sniper Rifle m_weaponAlias[Weapon::SG550] = "sg550"; // Sig SG-550 Sniper m_weaponAlias[Weapon::M249] = "m249"; // FN M249 Para m_weaponAlias[Weapon::Flashbang] = "flash"; // Concussion Grenade m_weaponAlias[Weapon::Explosive] = "hegren"; // High-Explosive Grenade m_weaponAlias[Weapon::Smoke] = "sgren"; // Smoke Grenade m_weaponAlias[Weapon::Armor] = "vest"; // Kevlar Vest m_weaponAlias[Weapon::ArmorHelm] = "vesthelm"; // Kevlar Vest and Helmet m_weaponAlias[Weapon::Defuser] = "defuser"; // Defuser Kit m_weaponAlias[Weapon::Shield] = "shield"; // Tactical Shield m_weaponAlias[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 (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 ()); } String authorStr = "Official Navigation Graph"; StringRef graphAuthor = graph.getAuthor (); if (!graphAuthor.startsWith (product.folder)) { authorStr.assignf ("Navigation Graph by: %s", graphAuthor); } 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.get (33, 255)) .writeByte (rg.get (33, 255)) .writeByte (rg.get (33, 255)) .writeByte (0) .writeByte (rg.get (230, 255)) .writeByte (rg.get (230, 255)) .writeByte (rg.get (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 (Revision: %s)\nDevised by %s\n\n%s", product.name, product.version, product.build.count, product.author, authorStr)); 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 (bots[survive]); } else { *pvHolder = reinterpret_cast (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 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.get (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.get (30, 40); average.second = rg.get (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 (average.first * 0.2f); int botPing = bot->m_basePing + rg.get (average.first - part, average.first + part) + rg.get (bot->m_difficulty / 2, bot->m_difficulty); int botLoss = rg.get (average.second / 2, average.second); if (botPing <= 5) { botPing = rg.get (10, 23); } else if (botPing > 70) { botPing = rg.get (30, 40); } 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.get (5, 10), rg.get (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_sendToDetour.detoured ()) { 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_sendToDetour.detoured ()) { m_sendToDetour.install (reinterpret_cast (BotSupport::sendTo), true); } } 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 &msg) -> int32 { return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength); }; auto packet = reinterpret_cast (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 (); for (uint8 i = 0; i < count; ++i) { buffer.skip (); // number buffer.skipString (); // name buffer.skip (); // score auto ctime = buffer.read (); // override connection time buffer.write (bots.getConnectTime (i, ctime)); } return send (buffer.data ()); } else if (packet[4] == 'I') { QueryBuffer buffer (packet, length, 5); buffer.skip (); // protocol // skip server name, folder, map game for (size_t i = 0; i < 4; ++i) { buffer.skipString (); } buffer.skip (); // steam app id buffer.skip (); // players buffer.skip (); // maxplayers buffer.skip (); // bots buffer.write (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 (0); // zero out bot count return send (buffer.data ()); } } return send ({ packet, length }); } StringRef BotSupport::weaponIdToAlias (int32 id) { StringRef none = "none"; if (m_weaponAlias.has (id)) { return m_weaponAlias[id]; } return none; }