Merge branch 'master' into chatter-fixes-and-improvements

This commit is contained in:
Владислав Сухов 2024-05-22 19:48:24 +00:00
commit 92af7495ea
16 changed files with 386 additions and 193 deletions

View file

@ -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"

View file

@ -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

115
inc/fakeping.h Normal file
View file

@ -0,0 +1,115 @@
//
// YaPB, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright © YaPB Project Developers <yapb@jeefo.net>.
//
// 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 <BotFakePingManager> {
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);

View file

@ -8,9 +8,6 @@
#pragma once
class BotSupport final : public Singleton <BotSupport> {
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);

View file

@ -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;

View file

@ -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',

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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 ();
}

129
src/fakeping.cpp Normal file
View file

@ -0,0 +1,129 @@
//
// YaPB, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright © YaPB Project Developers <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
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 <int> () >= 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 <int> (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 <int> (static_cast <float> (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 <float> ());
}
int BotFakePingManager::randomBase () const {
return rg (cv_ping_base_min.as <int> (), cv_ping_base_max.as <int> ());
}

View file

@ -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 <edict_t *> (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 <int> () == 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 <edict_t *> (reinterpret_cast <const edict_t *> (player));
if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_IGNORED);
if (fakeping.hasFeature ()) {
if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) {
fakeping.emit (ent);
}
}
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 <edict_t *> (reinterpret_cast <const edict_t *> (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;
}

View file

@ -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 <int> (), cv_ping_base_max.as <int> ());
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;

View file

@ -385,7 +385,7 @@ void Bot::postProcessGoals (const IntArray &goals, int result[]) {
return true;
}
}
return false;
return isOccupiedNode (index, true);
};
static IntArray resulting {};

View file

@ -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 <int> () != 2) {
return;
}
MutexScopedLock lock (m_cs);
Twin <int, int> 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 <int> (static_cast <float> (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));
}

View file

@ -60,6 +60,7 @@
<ClInclude Include="..\inc\constant.h" />
<ClInclude Include="..\inc\control.h" />
<ClInclude Include="..\inc\engine.h" />
<ClInclude Include="..\inc\fakeping.h" />
<ClInclude Include="..\inc\graph.h" />
<ClInclude Include="..\inc\hooks.h" />
<ClInclude Include="..\inc\manager.h" />
@ -98,6 +99,7 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\src\fakeping.cpp" />
<ClCompile Include="..\src\graph.cpp" />
<ClCompile Include="..\src\hooks.cpp" />
<ClCompile Include="..\src\linkage.cpp" />

View file

@ -192,6 +192,9 @@
<ClInclude Include="..\inc\hooks.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\inc\fakeping.h">
<Filter>inc</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\botlib.cpp">
@ -263,6 +266,9 @@
<ClCompile Include="..\src\hooks.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\fakeping.cpp">
<Filter>src</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="yapb.rc">