diff --git a/CMakeLists.txt b/CMakeLists.txt index c6c572f..7b93c5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ set(YAPB_SRC "src/config.cpp" "src/control.cpp" "src/engine.cpp" + "src/fakeping.cpp" "src/graph.cpp" "src/hooks.cpp" "src/linkage.cpp" diff --git a/README.md b/README.md index c49d3b8..7e5253c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## YaPB 🇺🇦 +## YaPB [![Latest YaPB](https://img.shields.io/github/v/release/yapb/yapb)](https://github.com/yapb/yapb/releases/latest) [![Latest YaPB](https://github.com/yapb/yapb/workflows/build/badge.svg)](https://github.com/yapb/yapb/actions) [![YaPB License](https://img.shields.io/github/license/yapb/yapb)](https://github.com/yapb/yapb/blob/master/LICENSE) [![Downloads](https://img.shields.io/github/downloads/yapb/yapb/total)](https://github.com/yapb/yapb/releases/latest) ## ☉ About diff --git a/inc/fakeping.h b/inc/fakeping.h new file mode 100644 index 0000000..be60c79 --- /dev/null +++ b/inc/fakeping.h @@ -0,0 +1,115 @@ +// +// YaPB, based on PODBot by Markus Klinge ("CountFloyd"). +// Copyright © YaPB Project Developers . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +// adopted from pingfaker amxx plugin +class PingBitMsg final { +private: + int32_t bits_ {}; + int32_t used_ {}; + + MessageWriter msg_ {}; + bool started_ {}; + +public: + enum : int32_t { + Single = 1, + PlayerID = 5, + Loss = 7, + Ping = 12 + }; + +public: + PingBitMsg () = default; + ~PingBitMsg () = default; + +public: + void write (int32_t bit, int32_t size) { + if (size > 32 - used_ || size < 1) { + return; + } + const auto maxSize = cr::bit (size); + + if (bit >= maxSize) { + bit = maxSize - 1; + } + bits_ = bits_ + (bit << used_); + used_ += size; + } + + void send (bool remaining = false) { + while (used_ >= 8) { + msg_.writeByte (bits_ & (cr::bit (8) - 1)); + bits_ = (bits_ >> 8); + used_ -= 8; + } + + if (remaining && used_ > 0) { + msg_.writeByte (bits_); + bits_ = used_ = 0; + } + } + + void start (edict_t *ent) { + if (started_) { + return; + } + msg_.start (MSG_ONE_UNRELIABLE, SVC_PINGS, nullptr, ent); + started_ = true; + } + + void flush () { + if (!started_) { + return; + } + write (0, Single); + send (true); + + started_ = false; + msg_.end (); + } +}; + +// bot fakeping manager +class BotFakePingManager final : public Singleton { +private: + mutable Mutex m_cs {}; + +private: + CountdownTimer m_recalcTime {}; + +public: + explicit BotFakePingManager () = default; + ~BotFakePingManager () = default; + +public: + // verify game supports fakeping and it's enabled + bool hasFeature () const; + + // reset the ping on disconnecting player + void reset (edict_t *ent); + + // calculate our own pings for all the bots + void syncCalculate (); + + // calculate our own pings for all the bots + void calculate (); + + // emit pings in update client data hook + void emit (edict_t *ent); + + // resetarts update timers + void restartTimer (); + + // get random base ping + int randomBase () const; +}; + +// expose fakeping manager +CR_EXPOSE_GLOBAL_SINGLETON (BotFakePingManager, fakeping); + diff --git a/inc/support.h b/inc/support.h index 3d77a86..b060a3d 100644 --- a/inc/support.h +++ b/inc/support.h @@ -8,9 +8,6 @@ #pragma once class BotSupport final : public Singleton { -private: - mutable Mutex m_cs {}; - private: bool m_needToSendWelcome {}; float m_welcomeReceiveTime {}; @@ -70,21 +67,6 @@ public: // update stats on clients void updateClients (); - // generates ping bitmask for SVC_PINGS message - int getPingBitmask (edict_t *ent, int loss, int ping); - - // calculate our own pings for all the players - void syncCalculatePings (); - - // calculate our own pings for all the players - void calculatePings (); - - // send modified pings to all the clients - void emitPings (edict_t *to); - - // reset ping to zero values - void resetPings (edict_t *to); - // checks if same model omitting the models directory bool isModel (const edict_t *ent, StringRef model); diff --git a/inc/yapb.h b/inc/yapb.h index ecc0a7e..5962b21 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -103,7 +103,6 @@ struct Client { int flags; // client flags int radio; // radio orders int menu; // identifier to opened menu - int ping; // when bot latency is enabled, client ping stored here int iconFlags[kGameMaxPlayers]; // flag holding chatter icons float iconTimestamp[kGameMaxPlayers]; // timers for chatter icons ClientNoise noise; @@ -506,6 +505,7 @@ private: void updatePredictedIndex (); void refreshCreatureStatus (char *infobuffer); void updateRightRef (); + void donateC4ToHuman (); void completeTask (); void executeTasks (); @@ -592,6 +592,9 @@ public: int m_difficulty {}; // bots hard level int m_moneyAmount {}; // amount of money in bot's bank + int m_pingBase {}; // base ping level for randomizing + int m_ping {}; // bot's acutal ping + float m_spawnTime {}; // time this bot spawned float m_timeTeamOrder {}; // time of last radio command float m_slowFrameTimestamp {}; // time to per-second think @@ -630,7 +633,6 @@ public: int m_blindNodeIndex {}; // node index to cover when blind int m_flashLevel {}; // flashlight level - int m_basePing {}; // base ping for bot int m_numEnemiesLeft {}; // number of enemies alive left on map int m_numFriendsLeft {}; // number of friend alive left on map int m_retryJoin {}; // retry count for choosing team/class @@ -880,6 +882,7 @@ private: #include "planner.h" #include "storage.h" #include "analyze.h" +#include "fakeping.h" // very global convars extern ConVar cv_jasonmode; diff --git a/meson.build b/meson.build index 1a624c8..e4ad17a 100644 --- a/meson.build +++ b/meson.build @@ -260,6 +260,7 @@ sources = files( 'src/config.cpp', 'src/control.cpp', 'src/engine.cpp', + 'src/fakeping.cpp', 'src/graph.cpp', 'src/hooks.cpp', 'src/linkage.cpp', diff --git a/src/botlib.cpp b/src/botlib.cpp index b0ff656..28a0098 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -2951,6 +2951,9 @@ void Bot::update () { m_hasC4 = !!(pev->weapons & cr::bit (Weapon::C4)); if (m_hasC4 && (cv_ignore_objectives || cv_jasonmode)) { + if (cv_ignore_objectives) { + donateC4ToHuman (); + } m_hasC4 = false; } } @@ -4189,3 +4192,58 @@ bool Bot::isCreature () { return m_isOnInfectedTeam || m_modelMask == kModelMaskZombie || m_modelMask == kModelMaskChicken; } + +void Bot::donateC4ToHuman () { + edict_t *recipient = nullptr; + + if (!m_hasC4) { + return; + } + const float radiusSq = cr::sqrf (1024.0f); + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team != m_team || client.ent == ent ()) { + continue; + } + + if (client.origin.distanceSq (pev->origin) < radiusSq) { + recipient = client.ent; + break; + } + } + + if (game.isNullEntity (recipient)) { + return; + } + m_itemCheckTime = game.time () + 1.0f; + + // select the bomb + if (m_currentWeapon != Weapon::C4) { + selectWeaponById (Weapon::C4); + } + dropCurrentWeapon (); + + // bomb on the ground entity + edict_t *bomb = nullptr; + + // search world for just dropped bomb + game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) { + if (util.isModel (ent, "backpack.mdl")) { + bomb = ent; + + if (!game.isNullEntity (bomb)) { + return EntitySearchResult::Break; + } + } + return EntitySearchResult::Continue; + }); + + // got c4 backpack + if (!game.isNullEntity (bomb)) { + bomb->v.flags |= FL_ONGROUND; + + // make recipient friend "pickup" it + MDLL_Touch (bomb, recipient); + } +} + diff --git a/src/combat.cpp b/src/combat.cpp index 6518d40..a62e8a1 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -993,15 +993,17 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) { // if we're have a glock or famas vary burst fire mode checkBurstMode (distance); - if (hasShield () && m_shieldCheckTime < game.time () && getCurrentTaskId () != Task::Camp) // better shield gun usage - { + // better shield gun usage + if (hasShield () && m_shieldCheckTime < game.time () && getCurrentTaskId () != Task::Camp) { + const bool hasEnemy = !game.isNullEntity (m_enemy); + if (distance >= 750.0f && !isShieldDrawn ()) { pev->button |= IN_ATTACK2; // draw the shield } else if (isShieldDrawn () || m_isReloading - || !seesEntity (m_enemy->v.origin) - || (!game.isNullEntity (m_enemy) && (m_enemy->v.button & IN_RELOAD))) { + || (hasEnemy && (m_enemy->v.button & IN_RELOAD)) + || (hasEnemy && !seesEntity (m_enemy->v.origin))) { pev->button |= IN_ATTACK2; // draw out the shield } diff --git a/src/engine.cpp b/src/engine.cpp index 2718c6b..c95e46b 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -90,6 +90,9 @@ void Game::levelInitialize (edict_t *entities, int max) { // set the global timer function timerStorage.setTimeAddress (&globals->time); + // restart the fakeping timer, so it'll start working after mapchange + fakeping.restartTimer (); + // go thru the all entities on map, and do whatever we're want for (int i = 0; i < max; ++i) { auto ent = entities + i; @@ -1007,9 +1010,6 @@ void Game::slowFrame () { // refresh bomb origin in case some plugin moved it out graph.setBombOrigin (); - // update client pings - util.calculatePings (); - // update next update time m_halfSecondFrame = nextUpdate * 0.25f + time (); } @@ -1049,6 +1049,9 @@ void Game::slowFrame () { // refresh bot infection (creature) status bots.refreshCreatureStatus (); + // update client pings + fakeping.calculate (); + // update next update time m_oneSecondFrame = nextUpdate + time (); } diff --git a/src/fakeping.cpp b/src/fakeping.cpp new file mode 100644 index 0000000..fd4b71b --- /dev/null +++ b/src/fakeping.cpp @@ -0,0 +1,129 @@ +// +// YaPB, based on PODBot by Markus Klinge ("CountFloyd"). +// Copyright © YaPB Project Developers . +// +// SPDX-License-Identifier: MIT +// + +#include + +ConVar cv_ping_base_min ("ping_base_min", "5", "Lower bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f); +ConVar cv_ping_base_max ("ping_base_max", "20", "Upper bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f); +ConVar cv_ping_count_real_players ("ping_count_real_players", "1", "Count player pings when calculating average ping for bots. If no, some random ping chosen for bots."); +ConVar cv_ping_updater_interval ("ping_updater_interval", "1.25", "Interval in which fakeping get updated in scoreboard.", true, 0.1f, 10.0f); + +bool BotFakePingManager::hasFeature () const { + return game.is (GameFlags::HasFakePings) && cv_show_latency.as () >= 2; +} + +void BotFakePingManager::reset (edict_t *to) { + + // no reset if game isn't support them + if (!hasFeature ()) { + return; + } + static PingBitMsg pbm {}; + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { + continue; + } + pbm.start (client.ent); + + pbm.write (1, PingBitMsg::Single); + pbm.write (game.indexOfPlayer (to), PingBitMsg::PlayerID); + pbm.write (0, PingBitMsg::Ping); + pbm.write (0, PingBitMsg::Loss); + + pbm.send (); + } + pbm.flush (); +} + +void BotFakePingManager::syncCalculate () { + MutexScopedLock lock (m_cs); + + int averagePing {}; + + if (cv_ping_count_real_players) { + int numHumans {}; + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { + continue; + } + numHumans++; + + int ping {}, loss {}; + engfuncs.pfnGetPlayerStats (client.ent, &ping, &loss); + + averagePing += ping > 0 && ping < 200 ? ping : randomBase (); + } + + if (numHumans > 0) { + averagePing /= numHumans; + } + else { + averagePing = randomBase (); + } + } + else { + averagePing = randomBase (); + } + + for (auto &bot : bots) { + auto botPing = static_cast (bot->m_pingBase + rg (averagePing - averagePing * 0.2f, averagePing + averagePing * 0.2f) + rg (bot->m_difficulty + 3, bot->m_difficulty + 6)); + + if (botPing < 5) { + botPing = rg (10, 15); + } + else if (botPing > 100) { + botPing = rg (30, 40); + } + bot->m_ping = static_cast (static_cast (bot->entindex () % 2 == 0 ? botPing * 0.25f : botPing * 0.5f)); + } +} + +void BotFakePingManager::calculate () { + if (!hasFeature ()) { + return; + } + + // throttle updating + if (!m_recalcTime.elapsed ()) { + return; + } + restartTimer (); + + worker.enqueue ([this] () { + syncCalculate (); + }); +} + +void BotFakePingManager::emit (edict_t *ent) { + if (!util.isPlayer (ent)) { + return; + } + static PingBitMsg pbm {}; + + for (const auto &bot : bots) { + pbm.start (ent); + + pbm.write (1, PingBitMsg::Single); + pbm.write (bot->entindex () - 1, PingBitMsg::PlayerID); + pbm.write (bot->m_ping, PingBitMsg::Ping); + pbm.write (0, PingBitMsg::Loss); + + pbm.send (); + } + pbm.flush (); +} + +void BotFakePingManager::restartTimer () { + m_recalcTime.start (cv_ping_updater_interval.as ()); +} + +int BotFakePingManager::randomBase () const { + return rg (cv_ping_base_min.as (), cv_ping_base_max.as ()); +} + diff --git a/src/linkage.cpp b/src/linkage.cpp index b4f1ffc..db61fe2 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -246,6 +246,28 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { dllapi.pfnClientDisconnect (ent); }; + + table->pfnClientPutInServer = [] (edict_t *ent) { + // this function is called once a just connected client actually enters the game, after + // having downloaded and synchronized its resources with the of the server's. It's the + // perfect place to hook for client connecting, since a client can always try to connect + // passing the ClientConnect() step, and not be allowed by the server later (because of a + // latency timeout or whatever reason). We can here keep track of both bots and players + // counts on occurence, since bots connect the server just like the way normal client do, + // and their third party bot flag is already supposed to be set then. If it's a bot which + // is connecting, we also have to awake its brain(s) by reading them from the disk. + + // refresh pings when client connetcs + if (fakeping.hasFeature ()) { + fakeping.emit (ent); + } + + if (game.is (GameFlags::Metamod)) { + RETURN_META (MRES_IGNORED); + } + dllapi.pfnClientPutInServer (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 @@ -389,22 +411,18 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { bots.frame (); }; - if (game.is (GameFlags::HasFakePings)) { - table->pfnCmdStart = [] (const edict_t *player, usercmd_t *cmd, unsigned int random_seed) { - auto ent = const_cast (player); + if (game.is (GameFlags::HasFakePings) && !game.is (GameFlags::Metamod)) { + table->pfnUpdateClientData = [] (const struct edict_s *player, int sendweapons, struct clientdata_s *cd) { + dllapi.pfnUpdateClientData (player, sendweapons, cd); - // 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 (cv_show_latency.as () == 2) { - if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button | cmd->buttons) & IN_SCORE) { - cmd->buttons &= ~IN_SCORE; - util.emitPings (ent); + // do a post-processing with non-metamod + auto ent = const_cast (reinterpret_cast (player)); + + if (fakeping.hasFeature ()) { + if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) { + fakeping.emit (ent); } } - - if (game.is (GameFlags::Metamod)) { - RETURN_META (MRES_IGNORED); - } - dllapi.pfnCmdStart (ent, cmd, random_seed); }; } @@ -479,6 +497,20 @@ CR_LINKAGE_C int GetEntityAPI_Post (gamefuncs_t *table, int) { RETURN_META (MRES_IGNORED); }; + if (game.is (GameFlags::HasFakePings)) { + table->pfnUpdateClientData = [] (const struct edict_s *player, int, struct clientdata_s *) { + // do a post-processing with non-metamod + auto ent = const_cast (reinterpret_cast (player)); + + if (fakeping.hasFeature ()) { + if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) { + fakeping.emit (ent); + } + } + RETURN_META (MRES_IGNORED); + }; + } + return HLTrue; } diff --git a/src/manager.cpp b/src/manager.cpp index af6a43f..3e3447d 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -39,9 +39,6 @@ ConVar cv_botskin_t ("botskin_t", "0", "Specifies the bots wanted skin for Terro ConVar cv_botskin_ct ("botskin_ct", "0", "Specifies the bots wanted skin for CT team.", true, 0.0f, 5.0f); ConVar cv_preferred_personality ("preferred_personality", "none", "Sets the default personality when creating bots with quota management.\nAllowed values: 'none', 'normal', 'careful', 'rusher'.\nIf 'none' is specified personality chosen randomly.", false); -ConVar cv_ping_base_min ("ping_base_min", "7", "Lower bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f); -ConVar cv_ping_base_max ("ping_base_max", "34", "Upper bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f); - ConVar cv_quota_adding_interval ("quota_adding_interval", "0.10", "Interval in which bots are added to the game.", true, 0.10f, 1.0f); ConVar cv_quota_maintain_interval ("quota_maintain_interval", "0.40", "Interval on which overall bot quota are checked.", true, 0.40f, 2.0f); @@ -846,7 +843,7 @@ void BotManager::setWeaponMode (int selection) { selection--; - constexpr int kStd[7][kNumWeapons] = { + constexpr int kStdMaps[7][kNumWeapons] = { {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only {-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only {-1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only @@ -856,7 +853,7 @@ void BotManager::setWeaponMode (int selection) { {-1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1} // Standard }; - constexpr int kAs[7][kNumWeapons] = { + constexpr int kAsMaps[7][kNumWeapons] = { {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only {-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only {-1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only @@ -872,8 +869,8 @@ void BotManager::setWeaponMode (int selection) { // set the correct weapon mode for (int i = 0; i < kNumWeapons; ++i) { - tab[i].teamStandard = kStd[selection][i]; - tab[i].teamAS = kAs[selection][i]; + tab[i].teamStandard = kStdMaps[selection][i]; + tab[i].teamAS = kAsMaps[selection][i]; } cv_jasonmode.set (selection == 0 ? 1 : 0); @@ -1161,7 +1158,8 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { } m_difficulty = rg (minDifficulty, maxDifficulty); } - m_basePing = rg (cv_ping_base_min.as (), cv_ping_base_max.as ()); + m_pingBase = fakeping.randomBase (); + m_ping = fakeping.randomBase (); m_previousThinkTime = game.time () - 0.1f; m_frameInterval = game.time (); @@ -1526,7 +1524,10 @@ void Bot::newRound () { m_followWaitTime = 0.0f; m_hostages.clear (); + + m_approachingLadderTimer.invalidate (); m_forgetLastVictimTimer.invalidate (); + m_lostReachableNodeTimer.invalidate (); for (auto &timer : m_chatterTimes) { timer = kMaxChatterRepeatInterval; @@ -1713,7 +1714,7 @@ void Bot::markStale () { showChatterIcon (false, true); // reset bots ping to default - util.resetPings (ent ()); + fakeping.reset (ent ()); // mark bot as leaving m_isStale = true; diff --git a/src/navigate.cpp b/src/navigate.cpp index eb272df..aef3cb5 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -385,7 +385,7 @@ void Bot::postProcessGoals (const IntArray &goals, int result[]) { return true; } } - return false; + return isOccupiedNode (index, true); }; static IntArray resulting {}; diff --git a/src/support.cpp b/src/support.cpp index 2d92e20..2bd4f7d 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -11,7 +11,6 @@ ConVar cv_display_welcome_text ("display_welcome_text", "1", "Enables or disable ConVar cv_enable_query_hook ("enable_query_hook", "0", "Enables or disables fake server queries response, that shows bots as real players in server browser."); ConVar cv_breakable_health_limit ("breakable_health_limit", "500.0", "Specifies the maximum health of breakable object, that bot will consider to destroy.", true, 1.0f, 3000.0); ConVar cv_enable_fake_steamids ("enable_fake_steamids", "0", "Allows or disallows bots to return fake steam id."); -ConVar cv_count_players_for_fakeping ("count_players_for_fakeping", "1", "Count player pings when calculating average ping for bots. If no, some random ping chosen for bots."); BotSupport::BotSupport () { m_needToSendWelcome = false; @@ -407,147 +406,6 @@ void BotSupport::updateClients () { } } -int BotSupport::getPingBitmask (edict_t *ent, int loss, int ping) { - // this function generates bitmask for SVC_PINGS engine message - // see: - // https://github.com/dreamstalker/rehlds/blob/a680f18ee1e7eb8c39fbdc45682163ca9477d783/rehlds/engine/sv_main.cpp#L4590 - - 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 () { - worker.enqueue ([this] () { - syncCalculatePings (); - }); -} - -void BotSupport::syncCalculatePings () { - if (!game.is (GameFlags::HasFakePings) || cv_show_latency.as () != 2) { - return; - } - MutexScopedLock lock (m_cs); - - Twin average { 0, 0 }; - int numHumans = 0; - - // only count player pings if we're allowed to - if (cv_count_players_for_fakeping) { - // 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); - - // @note: for those who asking on a email, we CAN call pfnGetPlayerStats hl-engine function in a separate thread - // since the function doesn't modify anything inside engine, so race-condition and crash isn't viable situation - // it's just fills ping and loss from engine structures, the only way to cause crash in separate thread - // is to call it with a invalid ``client`` pointer (on goldsrc), thus causing Con_Printf which is not compatible with - // multi-threaded environment - // - // see: - // https://github.com/dreamstalker/rehlds/blob/a680f18ee1e7eb8c39fbdc45682163ca9477d783/rehlds/engine/pr_cmds.cpp#L2735C15-L2735C32 - // https://github.com/fwgs/xash3d-fwgs/blob/f5b9826fd9bbbdc5293c1ff522de11ce28d3c9f2/engine/server/sv_game.c#L4443 - - // store normal client ping - client.ping = getPingBitmask (client.ent, loss, ping > 0 ? ping : rg (8, 16)); // getting player ping sometimes fails - ++numHumans; - - average.first += ping; - average.second += loss; - } - - if (numHumans > bots.getBotCount () / 4) { - average.first /= numHumans; - average.second /= numHumans; - } - else { - average.first = rg (10, 20); - average.second = rg (5, 10); - } - } - else { - average.first = rg (10, 20); - average.second = rg (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 interested in bots here - if (!bot) { - continue; - } - const int part = static_cast (static_cast (average.first) * 0.2f); - - int botPing = bot->m_basePing + rg (average.first - part, average.first + part) + rg (bot->m_difficulty / 2, bot->m_difficulty); - const int botLoss = rg (average.second / 2, average.second); - - if (botPing < 2) { - botPing = rg (10, 23); - } - else if (botPing > 100) { - botPing = rg (30, 40); - } - client.ping = getPingBitmask (client.ent, botLoss, botPing); - } -} - -void BotSupport::emitPings (edict_t *to) { - static MessageWriter msg {}; - - auto isThirdpartyBot = [] (edict_t *ent) { - return !bots[ent] && (ent->v.flags & FL_FAKECLIENT); - }; - - for (auto &client : m_clients) { - if (!(client.flags & ClientFlags::Used) || client.ent == game.getLocalEntity () || isThirdpartyBot (client.ent)) { - continue; - } - - // do not send to dormants - if (client.ent->v.flags & FL_DORMANT) { - continue; - } - - // no ping, no fun - if (!client.ping) { - client.ping = getPingBitmask (client.ent, rg (5, 10), rg (15, 40)); - } - - msg.start (MSG_ONE_UNRELIABLE, SVC_PINGS, nullptr, to) - .writeLong (client.ping) - .end (); - } -} - -void BotSupport::resetPings (edict_t *to) { - static MessageWriter msg {}; - - // no reset if game isn't support them - if (!game.is (GameFlags::HasFakePings)) { - return; - } - - for (auto &client : m_clients) { - if (!(client.flags & ClientFlags::Used) || isFakeClient (client.ent)) { - continue; - } - - msg.start (MSG_ONE_UNRELIABLE, SVC_PINGS, nullptr, client.ent) - .writeLong (getPingBitmask (to, 0, 0)) - .end (); - } -} - bool BotSupport::isModel (const edict_t *ent, StringRef model) { return model.startsWith (ent->v.model.chars (9)); } diff --git a/vc/yapb.vcxproj b/vc/yapb.vcxproj index 855fbc0..7883623 100644 --- a/vc/yapb.vcxproj +++ b/vc/yapb.vcxproj @@ -60,6 +60,7 @@ + @@ -98,6 +99,7 @@ true true + diff --git a/vc/yapb.vcxproj.filters b/vc/yapb.vcxproj.filters index 7c35720..5de97c1 100644 --- a/vc/yapb.vcxproj.filters +++ b/vc/yapb.vcxproj.filters @@ -192,6 +192,9 @@ inc + + inc + @@ -263,6 +266,9 @@ src + + src +