fix: bots at difficulty 0 unable to do anything useful

fix: lang configs unable to parse last translated line (fixes #340)
fix: last enemy isn't  cleared instantly with dead entity anymore
fix: bot weakness in pistol rounds
analyzer: improved optimization of useless nodes
linkage: make inability to call gamedll player( non-fatal
linkage: fixed bot boot  on WON engines pre 2000 builds (support for beta 6.5 restored)
cvars: added suupport to revert all cvars to defaults via 'yb cvars defaults'
cvars: added cv_preferred_personality  to select bot default personality
refactor: use single function to send hud messages over the bot code
bot: added random original podbot welcome message to preserve origins of this bot
conf: shuffle bot names and chatter items on conflig load
conf: simplified a bit chatter.cfg syntax (old syntax  still works
build: added support for building with CMake (thanks @Velaron)
refactor: rall the memory hooks moved into their one cpp file
This commit is contained in:
jeefo 2024-01-19 00:03:45 +03:00
commit bf91ef2831
No known key found for this signature in database
GPG key ID: 927BCA0779BEA8ED
35 changed files with 1256 additions and 734 deletions

View file

@ -52,6 +52,9 @@ void GraphAnalyze::update () {
if (m_updateInterval >= game.time ()) {
return;
}
else {
displayOverlayMessage ();
}
// add basic nodes
if (!m_basicsCreated) {
@ -157,6 +160,8 @@ void GraphAnalyze::finish () {
return;
}
vistab.startRebuild ();
ctrl.enableDrawModels (false);
cv_quota.revert ();
}
}
@ -171,13 +176,6 @@ void GraphAnalyze::optimize () {
}
cleanup ();
// clear the useless connections
if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) {
for (auto i = 0; i < graph.length (); ++i) {
graph.clearConnections (i);
}
}
auto smooth = [] (const Array <int> &nodes) {
Vector result;
@ -203,7 +201,7 @@ void GraphAnalyze::optimize () {
Array <int> indexes;
for (const auto &link : path.links) {
if (graph.exists (link.index) && !m_optimizedNodes[link.index] && cr::fequal (path.origin.z, graph[link.index].origin.z)) {
if (graph.exists (link.index) && !m_optimizedNodes[link.index] && !AStarAlgo::cantSkipNode (path.number, link.index, true)) {
indexes.emplace (link.index);
}
}
@ -218,6 +216,13 @@ void GraphAnalyze::optimize () {
graph.add (NodeAddFlag::Normal, pos);
}
}
// clear the useless connections
if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) {
for (auto i = 0; i < graph.length (); ++i) {
graph.clearConnections (i);
}
}
}
void GraphAnalyze::cleanup () {
@ -262,6 +267,37 @@ void GraphAnalyze::cleanup () {
}
}
void GraphAnalyze::displayOverlayMessage () {
auto listenserverEdict = game.getLocalEntity ();
if (game.isNullEntity (listenserverEdict) || !m_isAnalyzing) {
return;
}
constexpr StringRef analyzeHudMesssage =
"+--------------------------------------------------------+\n"
" Map analysis for bots is in progress. Please Wait.. \n"
"+--------------------------------------------------------+\n";
static hudtextparms_t textParams {};
textParams.channel = 1;
textParams.x = -1.0f;
textParams.y = -1.0f;
textParams.effect = 1;
textParams.r1 = textParams.r2 = static_cast <uint8_t> (255);
textParams.g1 = textParams.g2 = static_cast <uint8_t> (31);
textParams.b1 = textParams.b2 = static_cast <uint8_t> (75);
textParams.a1 = textParams.a2 = static_cast <uint8_t> (0);
textParams.fadeinTime = 0.0078125f;
textParams.fadeoutTime = 0.0078125f;
textParams.holdTime = m_updateInterval;
textParams.fxTime = 0.25f;
game.sendHudMessage (listenserverEdict, textParams, analyzeHudMesssage);
}
void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
range *= 0.75f;

View file

@ -1686,7 +1686,7 @@ void Bot::refreshEnemyPredict () {
if (distanceToLastEnemySq > cr::sqrf (128.0f) && (distanceToLastEnemySq < cr::sqrf (2048.0f) || usesSniper ())) {
m_aimFlags |= AimFlags::PredictPath;
}
const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f && distanceToLastEnemySq < cr::sqrf (256.0f);
const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f && distanceToLastEnemySq < cr::sqrf (256.0f) && m_shootTime + 2.5f > game.time ();
if (!denyLastEnemy && seesEntity (m_lastEnemyOrigin, true)) {
m_aimFlags |= AimFlags::LastEnemy;
@ -1778,19 +1778,14 @@ void Bot::setConditions () {
m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance);
m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance);
auto clearLastEnemy = [&] () {
m_lastEnemyOrigin = nullptr;
m_lastEnemy = nullptr;
};
// check if our current enemy is still valid
if (!game.isNullEntity (m_lastEnemy)) {
if (!util.isAlive (m_lastEnemy) && m_shootAtDeadTime < game.time ()) {
clearLastEnemy ();
m_lastEnemy = nullptr;
}
}
else {
clearLastEnemy ();
m_lastEnemy = nullptr;
}
// don't listen if seeing enemy, just checked for sounds or being blinded (because its inhuman)
@ -1798,8 +1793,20 @@ void Bot::setConditions () {
updateHearing ();
m_soundUpdateTime = game.time () + 0.25f;
}
else if (m_heardSoundTime < game.time ()) {
else if (m_heardSoundTime + 10.0f < game.time ()) {
m_states &= ~Sense::HearingEnemy;
// clear the last enemy pointers if time has passed or enemy far away
if (!m_lastEnemyOrigin.empty ()) {
auto distanceSq = pev->origin.distanceSq (m_lastEnemyOrigin);
if (distanceSq > cr::sqrf (2048.0f) || (game.isNullEntity (m_enemy) && m_seeEnemyTime + 10.0f < game.time ())) {
m_lastEnemyOrigin = nullptr;
m_lastEnemy = nullptr;
m_aimFlags &= ~AimFlags::LastEnemy;
}
}
}
refreshEnemyPredict ();
@ -2001,11 +2008,11 @@ void Bot::filterTasks () {
offensive = subsumeDesire (offensive, pickup); // if offensive task, don't allow picking up stuff
auto sub = maxDesire (offensive, def); // default normal & careful tasks against offensive actions
auto final = subsumeDesire (&filter[Task::Blind], maxDesire (survive, sub)); // reason about fleeing instead
auto finalTask = subsumeDesire (&filter[Task::Blind], maxDesire (survive, sub)); // reason about fleeing instead
if (!m_tasks.empty ()) {
final = maxDesire (final, getTask ());
startTask (final->id, final->desire, final->data, final->time, final->resume); // push the final behavior in our task stack to carry out
finalTask = maxDesire (finalTask, getTask ());
startTask (finalTask->id, finalTask->desire, finalTask->data, finalTask->time, finalTask->resume); // push the final behavior in our task stack to carry out
}
}
@ -2733,12 +2740,6 @@ void Bot::frame () {
kick ();
return;
}
// clear enemy far away
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::sqrf (2048.0f)) {
m_lastEnemy = nullptr;
m_lastEnemyOrigin = nullptr;
}
m_slowFrameTimestamp = game.time () + 0.5f;
}
@ -3097,7 +3098,7 @@ void Bot::showDebugOverlay () {
}
auto overlayEntity = graph.getEditor ();
if (overlayEntity->v.iuser2 == entindex ()) {
if (overlayEntity->v.iuser2 == entindex () && overlayEntity->v.origin.distanceSq (pev->origin) < cr::sqrf (256.0f)) {
displayDebugOverlay = true;
}
@ -3162,12 +3163,11 @@ void Bot::showDebugOverlay () {
if (m_tasks.empty ()) {
return;
}
const auto drawTime = globals->frametime * 500.0f;
if (tid != getCurrentTaskId () || index != m_currentNodeIndex || goal != getTask ()->data || m_timeDebugUpdateTime < game.time ()) {
tid = getCurrentTaskId ();
index = m_currentNodeIndex;
goal = getTask ()->data;
index = m_currentNodeIndex;
String enemy = "(none)";
@ -3191,31 +3191,38 @@ void Bot::showDebugOverlay () {
aimFlags.appendf (" %s", flags[static_cast <int32_t> (bit)]);
}
}
auto weapon = util.weaponIdToAlias (m_currentWeapon);
StringRef weapon = util.weaponIdToAlias (m_currentWeapon);
StringRef debugData = strings.format (
"\n\n\n\n\n%s (H:%.1f/A:%.1f)- Task: %d=%s Desire:%.02f\n"
"Item: %s Clip: %d Ammo: %d%s Money: %d AimFlags: %s\n"
"SP=%.02f SSP=%.02f I=%d PG=%d G=%d T: %.02f MT: %d\n"
"Enemy=%s Pickup=%s Type=%s Terrain=%s Stuck=%s\n",
pev->netname.str (), m_healthValue, pev->armorvalue,
tid, tasks[tid], getTask ()->desire, weapon, getAmmoInClip (),
getAmmo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim (),
m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - game.time (),
pev->movetype, enemy, pickup, personalities[m_personality], boolValue (m_checkTerrain),
boolValue (m_isStuck));
String debugData;
debugData.assignf ("\n\n\n\n\n%s (H:%.1f/A:%.1f)- Task: %d=%s Desire:%.02f\nItem: %s Clip: %d Ammo: %d%s Money: %d AimFlags: %s\nSP=%.02f SSP=%.02f I=%d PG=%d G=%d T: %.02f MT: %d\nEnemy=%s Pickup=%s Type=%s Terrain=%s Stuck=%s\n", pev->netname.str (), m_healthValue, pev->armorvalue, tid, tasks[tid], getTask ()->desire, weapon, getAmmoInClip (), getAmmo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim (), m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - game.time (), pev->movetype, enemy, pickup, personalities[m_personality], boolValue (m_checkTerrain), boolValue (m_isStuck));
static hudtextparms_t textParams {};
MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, overlayEntity)
.writeByte (TE_TEXTMESSAGE)
.writeByte (1)
.writeShort (MessageWriter::fs16 (-1.0f, 13.0f))
.writeShort (MessageWriter::fs16 (0.0f, 13.0f))
.writeByte (0)
.writeByte (m_team == Team::CT ? 0 : 255)
.writeByte (100)
.writeByte (m_team != Team::CT ? 0 : 255)
.writeByte (0)
.writeByte (255)
.writeByte (255)
.writeByte (255)
.writeByte (0)
.writeShort (MessageWriter::fu16 (0.0f, 8.0f))
.writeShort (MessageWriter::fu16 (0.0f, 8.0f))
.writeShort (MessageWriter::fu16 (drawTime, 8.0f))
.writeString (debugData.chars ());
textParams.channel = 1;
textParams.x = -1.0f;
textParams.y = 0.0f;
textParams.effect = 0;
m_timeDebugUpdateTime = game.time () + drawTime;
textParams.r1 = textParams.r2 = static_cast <uint8_t> (m_team == Team::CT ? 0 : 255);
textParams.g1 = textParams.g2 = static_cast <uint8_t> (100);
textParams.b1 = textParams.b2 = static_cast <uint8_t> (m_team != Team::CT ? 0 : 255);
textParams.a1 = textParams.a2 = static_cast <uint8_t> (1);
textParams.fadeinTime = 0.0f;
textParams.fadeoutTime = 0.0f;
textParams.holdTime = 0.5f;
textParams.fxTime = 0.0f;
game.sendHudMessage (overlayEntity, textParams, debugData);
m_timeDebugUpdateTime = game.time () + 0.5f;
}
// green = destination origin
@ -3714,7 +3721,7 @@ void Bot::updateHearing () {
}
// didn't bot already have an enemy ? take this one...
if (m_lastEnemyOrigin.empty () || m_lastEnemy == nullptr) {
if (m_lastEnemyOrigin.empty () || game.isNullEntity (m_lastEnemy)) {
m_lastEnemy = hearedEnemy;
m_lastEnemyOrigin = hearedEnemy->v.origin;
}

View file

@ -10,12 +10,37 @@
ConVar cv_chat ("chat", "1", "Enables or disables bots chat functionality.");
ConVar cv_chat_percent ("chat_percent", "30", "Bot chances to send random dead chat when killed.", true, 0.0f, 100.0f);
void BotSupport::stripTags (String &line) {
BotChatManager::BotChatManager () {
m_clanTags.emplace ("[[", "]]");
m_clanTags.emplace ("-=", "=-");
m_clanTags.emplace ("-[", "]-");
m_clanTags.emplace ("-]", "[-");
m_clanTags.emplace ("-}", "{-");
m_clanTags.emplace ("-{", "}-");
m_clanTags.emplace ("<[", "]>");
m_clanTags.emplace ("<]", "[>");
m_clanTags.emplace ("[-", "-]");
m_clanTags.emplace ("]-", "-[");
m_clanTags.emplace ("{-", "-}");
m_clanTags.emplace ("}-", "-{");
m_clanTags.emplace ("[", "]");
m_clanTags.emplace ("{", "}");
m_clanTags.emplace ("<", "[");
m_clanTags.emplace (">", "<");
m_clanTags.emplace ("-", "-");
m_clanTags.emplace ("|", "|");
m_clanTags.emplace ("=", "=");
m_clanTags.emplace ("+", "+");
m_clanTags.emplace ("(", ")");
m_clanTags.emplace (")", "(");
}
void BotChatManager::stripTags (String &line) {
if (line.empty ()) {
return;
}
for (const auto &tag : m_tags) {
for (const auto &tag : m_clanTags) {
const size_t start = line.find (tag.first, 0);
if (start != String::InvalidIndex) {
@ -30,7 +55,7 @@ void BotSupport::stripTags (String &line) {
}
}
void BotSupport::humanizePlayerName (String &playerName) {
void BotChatManager::humanizePlayerName (String &playerName) {
if (playerName.empty ()) {
return;
}
@ -49,7 +74,7 @@ void BotSupport::humanizePlayerName (String &playerName) {
}
}
void BotSupport::addChatErrors (String &line) {
void BotChatManager::addChatErrors (String &line) {
// sometimes switch name to lower characters, only valid for the english languge
if (rg.chance (8) && cv_language.str () == "en") {
line.lowercase ();
@ -72,7 +97,7 @@ void BotSupport::addChatErrors (String &line) {
}
}
bool BotSupport::checkKeywords (StringRef line, String &reply) {
bool BotChatManager::checkKeywords (StringRef line, String &reply) {
// this function checks is string contain keyword, and generates reply to it
if (!cv_chat.bool_ () || line.empty ()) {
@ -132,7 +157,7 @@ void Bot::prepareChatMessage (StringRef message) {
// must be called before return or on the end
auto finishPreparation = [&] () {
if (!m_chatBuffer.empty ()) {
util.addChatErrors (m_chatBuffer);
chatlib.addChatErrors (m_chatBuffer);
}
};
@ -153,7 +178,7 @@ void Bot::prepareChatMessage (StringRef message) {
return "unknown";
}
String playerName = ent->v.netname.chars ();
util.humanizePlayerName (playerName);
chatlib.humanizePlayerName (playerName);
return playerName;
};
@ -287,7 +312,7 @@ void Bot::prepareChatMessage (StringRef message) {
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);
return chatlib.checkKeywords (utf8tools.strToUpper (m_sayTextBuffer.sayText), reply);
}
bool Bot::isReplyingToChat () {
@ -316,8 +341,8 @@ bool Bot::isReplyingToChat () {
}
void Bot::checkForChat () {
// say a text every now and then
if (m_isAlive || !cv_chat.bool_ () || game.is (GameFlags::CSDM)) {
return;
}

View file

@ -386,7 +386,7 @@ bool Bot::lookupEnemies () {
if (other->m_seeEnemyTime + 2.0f < game.time () && game.isNullEntity (other->m_lastEnemy) && util.isVisible (pev->origin, other->ent ()) && other->isInViewCone (pev->origin)) {
other->m_lastEnemy = newEnemy;
other->m_lastEnemyOrigin = m_lastEnemyOrigin;
other->m_lastEnemyOrigin = newEnemy->v.origin;
other->m_seeEnemyTime = game.time ();
other->m_states |= (Sense::SuspectEnemy | Sense::HearingEnemy);
other->m_aimFlags |= AimFlags::LastEnemy;
@ -405,7 +405,7 @@ bool Bot::lookupEnemies () {
// shoot at dying players if no new enemy to give some more human-like illusion
if (m_seeEnemyTime + 0.1f > game.time ()) {
if (!usesSniper ()) {
m_shootAtDeadTime = game.time () + cr::clamp (m_agressionLevel * 1.25f, 0.25f, 0.45f);
m_shootAtDeadTime = game.time () + cr::clamp (m_agressionLevel * 1.25f, 0.15f, 0.25f);
m_actualReactionTime = 0.0f;
m_states |= Sense::SuspectEnemy;
@ -424,7 +424,11 @@ bool Bot::lookupEnemies () {
}
// if no enemy visible check if last one shoot able through wall
if (cv_shoots_thru_walls.bool_ () && rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct) && m_difficulty >= Difficulty::Normal && isPenetrableObstacle (newEnemy->v.origin)) {
if (cv_shoots_thru_walls.bool_ ()
&& m_difficulty >= Difficulty::Normal
&& rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct)
&& isPenetrableObstacle (newEnemy->v.origin)) {
m_seeEnemyTime = game.time ();
m_states |= Sense::SuspectEnemy;
@ -439,7 +443,15 @@ bool Bot::lookupEnemies () {
}
// check if bots should reload...
if ((m_aimFlags <= AimFlags::PredictPath && m_seeEnemyTime + 3.0f < game.time () && !(m_states & (Sense::SeeingEnemy | Sense::HearingEnemy)) && game.isNullEntity (m_lastEnemy) && game.isNullEntity (m_enemy) && getCurrentTaskId () != Task::ShootBreakable && getCurrentTaskId () != Task::PlantBomb && getCurrentTaskId () != Task::DefuseBomb) || bots.isRoundOver ()) {
if ((m_aimFlags <= AimFlags::PredictPath
&& m_seeEnemyTime + 3.0f < game.time ()
&& !(m_states & (Sense::SeeingEnemy | Sense::HearingEnemy))
&& game.isNullEntity (m_lastEnemy)
&& game.isNullEntity (m_enemy)
&& getCurrentTaskId () != Task::ShootBreakable
&& getCurrentTaskId () != Task::PlantBomb
&& getCurrentTaskId () != Task::DefuseBomb) || bots.isRoundOver ()) {
if (!m_reloadState) {
m_reloadState = Reload::Primary;
}
@ -458,20 +470,20 @@ bool Bot::lookupEnemies () {
}
Vector Bot::getBodyOffsetError (float distance) {
if (game.isNullEntity (m_enemy)) {
if (game.isNullEntity (m_enemy) || distance < kSprayDistance) {
return nullptr;
}
if (m_aimErrorTime < game.time ()) {
const float hitError = distance / (cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 1000.0f);
const float hitError = distance / (cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 1280.0f);
const auto &maxs = m_enemy->v.maxs, &mins = m_enemy->v.mins;
m_aimLastError = Vector (rg.get (mins.x * hitError, maxs.x * hitError), rg.get (mins.y * hitError, maxs.y * hitError), rg.get (mins.z * hitError, maxs.z * hitError));
m_aimLastError = Vector (rg.get (mins.x * hitError, maxs.x * hitError), rg.get (mins.y * hitError, maxs.y * hitError), rg.get (mins.z * hitError * 0.5f, maxs.z * hitError * 0.5f));
const auto &aimError = conf.getDifficultyTweaks (m_difficulty) ->aimError;
m_aimLastError += Vector (rg.get (-aimError.x, aimError.x), rg.get (-aimError.y, aimError.y), rg.get (-aimError.z, aimError.z));
m_aimErrorTime = game.time () + rg.get (1.5f, 2.0f);
m_aimErrorTime = game.time () + rg.get (0.4f, 0.8f);
}
return m_aimLastError;
}
@ -514,7 +526,7 @@ Vector Bot::getBodyOffsetError (float distance) {
else if (util.isPlayer (m_enemy)) {
// now take in account different parts of enemy body
if (m_enemyParts & (Visibility::Head | Visibility::Body)) {
auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct;
const auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct;
// now check is our skill match to aim at head, else aim at enemy body
if (rg.chance (headshotPct)) {
@ -570,7 +582,7 @@ Vector Bot::getCustomHeight (float distance) {
{ 1.5f, -4.0f, -9.0f } // heavy
};
// only highskilled bots do that
// only high-skilled bots do that
if (m_difficulty != Difficulty::Expert || (m_enemy->v.flags & FL_DUCKING)) {
return 0.0f;
}
@ -987,12 +999,19 @@ void Bot::fireWeapons () {
// loop through all the weapons until terminator is found...
while (tab[selectIndex].id) {
const auto wid = tab[selectIndex].id;
// is the bot carrying this weapon?
if (weapons & cr::bit (tab[selectIndex].id)) {
if (weapons & cr::bit (wid)) {
// is enough ammo available to fire AND check is better to use pistol in our current situation...
if (m_ammoInClip[tab[selectIndex].id] > 0 && !isWeaponBadAtDistance (selectIndex, distance)) {
choosenWeapon = selectIndex;
if (m_ammoInClip[wid] > 0 && !isWeaponBadAtDistance (selectIndex, distance)) {
const auto &prop = conf.getWeaponProp (wid);
// skip the weapons that cannot be used underwater (regamedll addition)
if (!(pev->waterlevel == 3 && (prop.flags & ITEM_FLAG_NOFIREUNDERWATER))) {
choosenWeapon = selectIndex;
}
}
}
selectIndex++;
@ -1005,11 +1024,11 @@ void Bot::fireWeapons () {
// loop through all the weapons until terminator is found...
while (tab[selectIndex].id) {
const int id = tab[selectIndex].id;
const int wid = tab[selectIndex].id;
// is the bot carrying this weapon?
if (weapons & cr::bit (id)) {
if (getAmmo (id) >= tab[selectIndex].minPrimaryAmmo) {
if (weapons & cr::bit (wid)) {
if (getAmmo (wid) >= tab[selectIndex].minPrimaryAmmo) {
// available ammo found, reload weapon
if (m_reloadState == Reload::None || m_reloadCheckTime > game.time ()) {
@ -1195,7 +1214,7 @@ void Bot::attackMovement () {
}
}
}
else if (rg.get (0, 100) < (isInNarrowPlace () ? 25 : 75) || usesKnife ()) {
else if (usesKnife ()) {
m_fightStyle = Fight::Strafe;
}
else {
@ -1217,10 +1236,6 @@ void Bot::attackMovement () {
m_fightStyle = Fight::Strafe;
}
if (usesPistol () && distance < 768.0f) {
m_fightStyle = Fight::Strafe;
}
if (m_fightStyle == Fight::Strafe) {
auto swapStrafeCombatDir = [&] () {
m_combatStrafeDir = (m_combatStrafeDir == Dodge::Left ? Dodge::Right : Dodge::Left);

View file

@ -144,6 +144,7 @@ void BotConfig::loadNamesConfig () {
}
file.close ();
}
m_botNames.shuffle ();
}
void BotConfig::loadWeaponsConfig () {
@ -345,6 +346,7 @@ void BotConfig::loadChatterConfig () {
if (event.str == items.first ()) {
// this does common work of parsing comma-separated chatter line
auto sentences = items[1].split (",");
sentences.shuffle ();
for (auto &sound : sentences) {
sound.trim ().trim ("\"");
@ -480,6 +482,10 @@ void BotConfig::loadLanguageConfig () {
String temp;
Twin <String, String> lang;
auto pushTranslatedMsg = [&] () {
m_language[hashLangString (lang.first.trim ().chars ())] = lang.second.trim ();
};
// clear all the translations before new load
m_language.clear ();
@ -494,7 +500,7 @@ void BotConfig::loadLanguageConfig () {
}
if (!lang.second.empty () && !lang.first.empty ()) {
m_language[hashLangString (lang.first.trim ().chars ())] = lang.second.trim ();
pushTranslatedMsg ();
}
}
else if (line.startsWith ("[TRANSLATED]") && !temp.empty ()) {
@ -503,6 +509,12 @@ void BotConfig::loadLanguageConfig () {
else {
temp += line;
}
// make sure last string is translated
if (file.eof () && !lang.first.empty ()) {
lang.second = line.trim ();
pushTranslatedMsg ();
}
}
file.close ();
}

View file

@ -195,7 +195,29 @@ int BotControl::cmdList () {
int BotControl::cmdCvars () {
enum args { alias = 1, pattern };
const auto &match = strValue (pattern);
auto match = strValue (pattern);
// revert all the cvars to their default values
if (match == "defaults") {
msg ("Bots cvars has been reverted to their default values.");
for (const auto &cvar : game.getCvars ()) {
if (!cvar.self || !cvar.self->ptr || cvar.type == Var::GameRef) {
continue;
}
// set depending on cvar type
if (cvar.bounded) {
cvar.self->set (cvar.initial);
}
else {
cvar.self->set (cvar.init.chars ());
}
}
cv_quota.revert (); // quota should be reverted instead of regval
return BotCommandResult::Handled;
}
const bool isSaveMain = match == "save";
const bool isSaveMap = match == "save_map";
@ -2162,11 +2184,9 @@ bool BotControl::handleMenuCommands (edict_t *ent) {
}
void BotControl::enableDrawModels (bool enable) {
StringArray entities;
entities.push ("info_player_start");
entities.push ("info_player_deathmatch");
entities.push ("info_vip_start");
static StringArray entities {
"info_player_start", "info_player_deathmatch", "info_vip_start"
};
if (enable) {
game.setPlayerStartDrawModels ();

View file

@ -81,7 +81,7 @@ void Game::levelInitialize (edict_t *entities, int max) {
bots.initQuota ();
// install the sendto hook to fake queries
util.installSendTo ();
fakequeries.init ();
// flush any print queue
ctrl.resetFlushTimestamp ();
@ -332,11 +332,13 @@ void Game::registerEngineCommand (const char *command, void func ()) {
// that for every "command_name" server command it receives, it should call the function
// pointed to by "function" in order to handle it.
// check for hl pre 1.1.0.4, as it's doesn't have pfnAddServerCommand
// check for hl pre 1.1.0.4, as it's doesn't have pfnAddServerCommand and many more stuff we need to work
if (!plat.isValidPtr (engfuncs.pfnAddServerCommand)) {
logger.fatal ("%s's minimum HL engine version is 1.1.0.4 and minimum Counter-Strike is Beta 6.5. Please update your engine / game version.", product.name);
}
engfuncs.pfnAddServerCommand (const_cast <char *> (command), func);
else {
engfuncs.pfnAddServerCommand (command, func);
}
}
void Game::playSound (edict_t *ent, const char *sound) {
@ -392,8 +394,8 @@ bool Game::checkVisibility (edict_t *ent, uint8_t *set) {
}
uint8_t *Game::getVisibilitySet (Bot *bot, bool pvs) {
if (is (GameFlags::Xash3D)) {
return nullptr; // TODO: bug fixed in upstream xash3d, should be removed
if (is (GameFlags::Xash3DLegacy)) {
return nullptr;
}
auto eyes = bot->getEyesPos ();
@ -460,6 +462,37 @@ void Game::sendServerMessage (StringRef message) {
engfuncs.pfnServerPrint (message.chars ());
}
void Game::sendHudMessage (edict_t *ent, const hudtextparms_t &htp, StringRef message) {
constexpr size_t maxSendLength = 512;
if (game.isNullEntity (ent)) {
return;
}
MessageWriter msg (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent);
msg.writeByte (TE_TEXTMESSAGE);
msg.writeByte (htp.channel & 0xff);
msg.writeShort (MessageWriter::fs16 (htp.x, 13.0f));
msg.writeShort (MessageWriter::fs16 (htp.y, 13.0f));
msg.writeByte (htp.effect);
msg.writeByte (htp.r1);
msg.writeByte (htp.g1);
msg.writeByte (htp.b1);
msg.writeByte (htp.a1);
msg.writeByte (htp.r2);
msg.writeByte (htp.g2);
msg.writeByte (htp.b2);
msg.writeByte (htp.a2);
msg.writeShort (MessageWriter::fu16 (htp.fadeinTime, 8.0f));
msg.writeShort (MessageWriter::fu16 (htp.fadeoutTime, 8.0f));
msg.writeShort (MessageWriter::fu16 (htp.holdTime, 8.0f));
if (htp.effect == 2) {
msg.writeShort (MessageWriter::fu16 (htp.fxTime, 8.0f));
}
msg.writeString (message.substr (0, maxSendLength).chars ());
}
void Game::prepareBotArgs (edict_t *ent, String str) {
// the purpose of this function is to provide fakeclients (bots) with the same client
// command-scripting advantages (putting multiple commands in one line between semicolons)
@ -726,7 +759,7 @@ bool Game::loadCSBinary () {
if (!m_gameLib) {
logger.fatal ("Unable to load gamedll \"%s\". Exiting... (gamedir: %s)", dll, mod);
}
auto ent = m_gameLib.resolve <EntityFunction> ("trigger_random_unique");
auto ent = m_gameLib.resolve <EntityProto> ("trigger_random_unique");
// detect regamedll by addon entity they provide
if (ent != nullptr) {
@ -765,7 +798,12 @@ bool Game::loadCSBinary () {
}
// detect if we're running modern game
auto entity = m_gameLib.resolve <EntityFunction> ("weapon_famas");
auto entity = m_gameLib.resolve <EntityProto> ("weapon_famas");
// detect legacy xash3d branch
if (engfuncs.pfnCVarGetPointer ("build") != nullptr) {
m_gameFlags |= GameFlags::Xash3DLegacy;
}
// detect xash engine
if (engfuncs.pfnCVarGetPointer ("host_ver") != nullptr) {
@ -855,6 +893,9 @@ bool Game::postload () {
// initialize weapons
conf.initWeapons ();
// register engine lib handle
m_engineLib.locate (reinterpret_cast <void *> (engfuncs.pfnPrecacheModel));
if (plat.android) {
m_gameFlags |= (GameFlags::Xash3D | GameFlags::Mobility | GameFlags::HasBotVoice | GameFlags::ReGameDLL);
@ -1245,7 +1286,7 @@ float LightMeasure::getLightLevel (const Vector &point) {
auto recursiveCheck = [&] () -> bool {
if (!isSoftRenderer) {
if (is25Anniversary) {
return recursiveLightPoint <msurface_hw_25anniversary_t, mnode_hw_t> (reinterpret_cast <mnode_hw_t *> (m_worldModel->nodes), point, endPoint);
return recursiveLightPoint <msurface_hw_hl25_t, mnode_hw_t> (reinterpret_cast <mnode_hw_t *> (m_worldModel->nodes), point, endPoint);
}
return recursiveLightPoint <msurface_hw_t, mnode_hw_t> (reinterpret_cast <mnode_hw_t *> (m_worldModel->nodes), point, endPoint);
}

View file

@ -12,9 +12,9 @@
// other platforms, and you want to run bot on it without metamod, consider enabling LINKENT_STATIC_THUNKS
// when compiling the bot, to get it supported.
#if defined(LINKENT_STATIC_THUNKS)
void forwardEntity_helper (EntityFunction &addr, const char *name, entvars_t *pev) {
void forwardEntity_helper (EntityProto &addr, const char *name, entvars_t *pev) {
if (!addr) {
addr = game.lib ().resolve <EntityFunction> (name);
addr = game.lib ().resolve <EntityProto> (name);
}
if (!addr) {
return;
@ -24,7 +24,7 @@ void forwardEntity_helper (EntityFunction &addr, const char *name, entvars_t *pe
#define LINK_ENTITY(entityName) \
CR_EXPORT void entityName (entvars_t *pev) { \
static EntityFunction addr; \
static EntityProto addr; \
forwardEntity_helper (addr, __FUNCTION__, pev); \
}

View file

@ -412,13 +412,14 @@ void BotGraph::addPath (int addIndex, int pathIndex, float distance) {
return;
}
}
auto integerDistance = cr::abs (static_cast <int> (distance));
// check for free space in the connection indices
for (auto &link : path.links) {
if (link.index == kInvalidNodeIndex) {
link.index = static_cast <int16_t> (pathIndex);
link.distance = cr::abs (static_cast <int> (distance));
link.distance = integerDistance;
msg ("Path added from %d to %d.", addIndex, pathIndex);
return;
}
@ -439,7 +440,7 @@ void BotGraph::addPath (int addIndex, int pathIndex, float distance) {
msg ("Path added from %d to %d.", addIndex, pathIndex);
path.links[slot].index = static_cast <int16_t> (pathIndex);
path.links[slot].distance = cr::abs (static_cast <int> (distance));
path.links[slot].distance = integerDistance;
}
}
@ -498,11 +499,11 @@ int BotGraph::getNearestNoBuckets (const Vector &origin, const float range, int
return index;
}
int BotGraph::getEditorNearest () {
int BotGraph::getEditorNearest (const float maxRange) {
if (!hasEditFlag (GraphEdit::On)) {
return kInvalidNodeIndex;
}
return getNearestNoBuckets (m_editor->v.origin, 50.0f);
return getNearestNoBuckets (m_editor->v.origin, maxRange);
}
int BotGraph::getNearest (const Vector &origin, const float range, int flags) {
@ -625,12 +626,12 @@ void BotGraph::add (int type, const Vector &pos) {
return;
case NodeAddFlag::JumpStart:
index = getEditorNearest ();
index = getEditorNearest (25.0f);
if (index != kInvalidNodeIndex && m_paths[index].number >= 0) {
const float distanceSq = m_editor->v.origin.distanceSq (m_paths[index].origin);
if (distanceSq < cr::sqrf (50.0f)) {
if (distanceSq < cr::sqrf (25.0f)) {
addNewNode = false;
path = &m_paths[index];
@ -643,12 +644,12 @@ void BotGraph::add (int type, const Vector &pos) {
break;
case NodeAddFlag::JumpEnd:
index = getEditorNearest ();
index = getEditorNearest (25.0f);
if (index != kInvalidNodeIndex && m_paths[index].number >= 0) {
const float distanceSq = m_editor->v.origin.distanceSq (m_paths[index].origin);
if (distanceSq < cr::sqrf (50.0f)) {
if (distanceSq < cr::sqrf (25.0f)) {
addNewNode = false;
path = &m_paths[index];
@ -1064,14 +1065,16 @@ void BotGraph::pathCreate (char dir) {
if (!isConnected (nodeFrom, nodeTo)) {
addPath (nodeFrom, nodeTo, distance);
}
for (auto &link : m_paths[nodeFrom].links) {
if (link.index == nodeTo && !(link.flags & PathFlag::Jump)) {
link.flags |= PathFlag::Jump;
m_paths[nodeFrom].radius = 0.0f;
msg ("Path added from %d to %d.", nodeFrom, nodeTo);
}
else if (link.index == nodeTo && (link.flags & PathFlag::Jump)) {
msg ("Denied path creation from %d to %d (path already exists).", nodeFrom, nodeTo);
msg ("Denied path creation from %d to %d (path already exists).", nodeFrom, nodeTo);
}
}
}
@ -1415,7 +1418,7 @@ void BotGraph::initNarrowPlaces () {
constexpr int32_t kNarrowPlacesMinGraphVersion = 2;
// if version 2 or higher, narrow places already initialized and saved into file
if (m_graphHeader.version >= kNarrowPlacesMinGraphVersion) {
if (m_graphHeader.version >= kNarrowPlacesMinGraphVersion && !hasEditFlag (GraphEdit::On)) {
m_narrowChecked = true;
return;
}
@ -2141,25 +2144,25 @@ void BotGraph::frame () {
}
static int channel = 0;
auto sendHudMessage = [] (Color color, float x, float y, edict_t *to, StringRef text) {
MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, to)
.writeByte (TE_TEXTMESSAGE)
.writeByte (channel++ & 0xff) // channel
.writeShort (MessageWriter::fs16 (x, 13.0f)) // x
.writeShort (MessageWriter::fs16 (y, 13.0f)) // y
.writeByte (0) // effect
.writeByte (color.red) // r1
.writeByte (color.green) // g1
.writeByte (color.blue) // b1
.writeByte (1) // a1
.writeByte (color.red) // r2
.writeByte (color.green) // g2
.writeByte (color.blue) // b2
.writeByte (1) // a2
.writeShort (0) // fadeintime
.writeShort (0) // fadeouttime
.writeShort (MessageWriter::fu16 (1.0f, 8.0f)) // holdtime
.writeString (text.chars ());
auto sendHudMessage = [&] (Color color, float x, float y, StringRef text) {
static hudtextparms_t textParams {};
textParams.channel = channel;
textParams.x = x;
textParams.y = y;
textParams.effect = 0;
textParams.r1 = textParams.r2 = static_cast <uint8_t> (color.red);
textParams.g1 = textParams.g2 = static_cast <uint8_t> (color.green);
textParams.b1 = textParams.b2 = static_cast <uint8_t> (color.blue);
textParams.a1 = textParams.a2 = static_cast <uint8_t> (1);
textParams.fadeinTime = 0.0f;
textParams.fadeoutTime = 0.0f;
textParams.holdTime = m_pathDisplayTime;
textParams.fxTime = 0.0f;
game.sendHudMessage (m_editor, textParams, text);
if (channel > 3) {
channel = 0;
@ -2207,16 +2210,16 @@ void BotGraph::frame () {
};
// display some information
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.025f, m_editor, getNodeData ("Current", nearestIndex));
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.025f, getNodeData ("Current", nearestIndex));
// check if we need to show the cached point index
if (m_cacheNodeIndex != kInvalidNodeIndex) {
sendHudMessage ({ 255, 255, 255 }, 0.28f, 0.16f, m_editor, getNodeData ("Cached", m_cacheNodeIndex));
sendHudMessage ({ 255, 255, 255 }, 0.28f, 0.16f, getNodeData ("Cached", m_cacheNodeIndex));
}
// check if we need to show the facing point index
if (m_facingAtIndex != kInvalidNodeIndex) {
sendHudMessage ({ 255, 255, 255 }, 0.28f, 0.025f, m_editor, getNodeData ("Facing", m_facingAtIndex));
sendHudMessage ({ 255, 255, 255 }, 0.28f, 0.025f, getNodeData ("Facing", m_facingAtIndex));
}
String timeMessage = strings.format (" Map: %s, Time: %s\n", game.getMapName (), util.getCurrentDateTime ());
@ -2230,10 +2233,10 @@ void BotGraph::frame () {
" CT: %d / %d\n"
" T: %d / %d\n\n", dangerIndexCT, dangerIndexCT != kInvalidNodeIndex ? practice.getDamage (Team::CT, nearestIndex, dangerIndexCT) : 0, dangerIndexT, dangerIndexT != kInvalidNodeIndex ? practice.getDamage (Team::Terrorist, nearestIndex, dangerIndexT) : 0);
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, m_editor, practiceText + timeMessage);
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, practiceText + timeMessage);
}
else {
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, m_editor, timeMessage);
sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, timeMessage);
}
}
}
@ -2350,10 +2353,10 @@ bool BotGraph::checkNodes (bool teleportPlayer) {
}
// perform DFS instead of floyd-warshall, this shit speedup this process in a bit
const auto length = cr::min (static_cast <size_t> (kMaxNodes), m_paths.length ());
const auto length = cr::min (static_cast <size_t> (kMaxNodes), m_paths.length ());
// ensure valid capacity
assert (length > 8 && length < static_cast <size_t> (kMaxNodes));
assert (length > 8 && length < static_cast <size_t> (kMaxNodes));
PathWalk walk;
walk.init (length);

179
src/hooks.cpp Normal file
View file

@ -0,0 +1,179 @@
//
// YaPB, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright © YaPB Project Developers <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) {
const auto send = [&] (const Twin <const uint8_t *, size_t> &msg) -> int32_t {
return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength);
};
auto packet = reinterpret_cast <const uint8_t *> (message);
constexpr int32_t packetLength = 5;
// player replies response
if (length > packetLength && memcmp (packet, "\xff\xff\xff\xff", packetLength - 1) == 0) {
if (packet[4] == 'D') {
QueryBuffer buffer { packet, length, packetLength };
auto count = buffer.read <uint8_t> ();
for (uint8_t i = 0; i < count; ++i) {
buffer.skip <uint8_t> (); // number
auto name = buffer.readString (); // name
buffer.skip <int32_t> (); // score
auto ctime = buffer.read <float> (); // override connection time
buffer.write <float> (bots.getConnectTime (name, ctime));
}
return send (buffer.data ());
}
else if (packet[4] == 'I') {
QueryBuffer buffer { packet, length, packetLength };
buffer.skip <uint8_t> (); // 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_t> (); // players
buffer.skip <uint8_t> (); // maxplayers
buffer.skip <uint8_t> (); // bots
buffer.write <uint8_t> (0); // zero out bot count
return send (buffer.data ());
}
else if (packet[4] == 'm') {
QueryBuffer buffer { packet, length, packetLength };
buffer.shiftToEnd (); // shift to the end of buffer
buffer.write <uint8_t> (0); // zero out bot count
return send (buffer.data ());
}
}
return send ({ packet, length });
}
void ServerQueryHook::init () {
// if previously requested to disable?
if (!cv_enable_query_hook.bool_ ()) {
if (m_sendToDetour.detoured ()) {
disable ();
}
return;
}
// do not enable on not dedicated server
if (!game.isDedicated ()) {
return;
}
SendToProto *sendToAddress = sendto;
// linux workaround with sendto
if (!plat.win && !plat.isNonX86 ()) {
if (game.elib ()) {
auto address = game.elib ().resolve <SendToProto *> ("sendto");
if (address != nullptr) {
sendToAddress = address;
}
}
}
m_sendToDetour.initialize ("ws2_32.dll", "sendto", sendToAddress);
// enable only on modern games
if (!game.is (GameFlags::Legacy) && (plat.nix || plat.win) && !plat.isNonX86 () && !m_sendToDetour.detoured ()) {
m_sendToDetour.install (reinterpret_cast <void *> (BotSupport::sendTo), true);
}
}
SharedLibrary::Func DynamicLinkerHook::lookup (SharedLibrary::Handle module, const char *function) {
static const auto &gamedll = game.lib ().handle ();
static const auto &self = m_self.handle ();
const auto resolve = [&] (SharedLibrary::Handle handle) {
return m_dlsym (handle, function);
};
if (entlink.needsBypass () && !strcmp (function, "CreateInterface")) {
entlink.setPaused (true);
auto ret = resolve (module);
entlink.disable ();
return ret;
}
// if requested module is yapb module, put in cache the looked up symbol
if (self != module) {
return resolve (module);
}
#if defined (CR_WINDOWS)
if (HIWORD (function) == 0) {
return resolve (module);
}
#endif
if (m_exports.exists (function)) {
return m_exports[function];
}
auto botAddr = resolve (self);
if (!botAddr) {
auto gameAddr = resolve (gamedll);
if (gameAddr) {
return m_exports[function] = gameAddr;
}
}
else {
return m_exports[function] = botAddr;
}
return nullptr;
}
bool DynamicLinkerHook::callPlayerFunction (edict_t *ent) {
auto callPlayer = [&] () {
reinterpret_cast <EntityProto> (m_exports["player"]) (&ent->v);
};
if (m_exports.exists ("player")) {
callPlayer ();
return true;
}
auto playerFunction = game.lib ().resolve <EntityProto> ("player");
if (!playerFunction) {
logger.error ("Cannot resolve player() function in GameDLL.");
return false;
}
m_exports["player"] = reinterpret_cast <SharedLibrary::Func> (playerFunction);
callPlayer ();
return true;
}
bool DynamicLinkerHook::needsBypass () const {
return !plat.win && !game.isDedicated ();
}
void DynamicLinkerHook::initialize () {
if (plat.isNonX86 () || game.is (GameFlags::Metamod)) {
return;
}
m_dlsym.initialize ("kernel32.dll", "GetProcAddress", DLSYM_FUNCTION);
m_dlsym.install (reinterpret_cast <void *> (lookupHandler), true);
if (needsBypass ()) {
m_dlclose.initialize ("kernel32.dll", "FreeLibrary", DLCLOSE_FUNCTION);
m_dlclose.install (reinterpret_cast <void *> (closeHandler), true);
}
m_self.locate (&engfuncs);
}

View file

@ -32,7 +32,58 @@ plugin_info_t Plugin_info = {
PT_ANYTIME, // when unloadable
};
CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
// compilers can't create lambdas with vaargs, so put this one in it's own namespace
namespace Hooks {
void handler_engClientCommand (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
if (game.isNullEntity (ent)) {
if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_SUPERCEDE);
}
return;
}
va_list ap;
auto buffer = strings.chars ();
va_start (ap, format);
vsnprintf (buffer, StringBuffer::StaticBufferSize, format, ap);
va_end (ap);
if (util.isFakeClient (ent) && !(ent->v.flags & FL_DORMANT)) {
auto bot = bots[ent];
if (bot) {
game.botCommand (bot->pev->pContainingEntity, 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 interfaceVersion) {
// 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
@ -49,7 +100,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
auto api_GetEntityAPI = game.lib ().resolve <decltype (&GetEntityAPI)> (__func__);
// pass other DLLs engine callbacks to function table...
if (!api_GetEntityAPI || api_GetEntityAPI (&dllapi, INTERFACE_VERSION) == 0) {
if (!api_GetEntityAPI || api_GetEntityAPI (&dllapi, interfaceVersion) == 0) {
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __func__);
}
dllfuncs.dllapi_table = &dllapi;
@ -294,7 +345,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
dllapi.pfnServerDeactivate ();
// refill export table
ents.flush ();
entlink.flush ();
};
table->pfnStartFrame = [] () {
@ -447,12 +498,12 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) {
plat.bzero (table, sizeof (enginefuncs_t));
}
if (ents.needsBypass () && !game.is (GameFlags::Metamod)) {
if (entlink.needsBypass () && !game.is (GameFlags::Metamod)) {
table->pfnCreateNamedEntity = [] (string_t classname) -> edict_t *{
if (ents.isPaused ()) {
ents.enable ();
ents.setPaused (false);
if (entlink.isPaused ()) {
entlink.enable ();
entlink.setPaused (false);
}
return engfuncs.pfnCreateNamedEntity (classname);
};
@ -601,6 +652,9 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) {
engfuncs.pfnWriteEntity (value);
};
// very ancient engine versions (pre 2xxx builds) needs this to work correctly
table->pfnClientCommand = Hooks::handler_engClientCommand;
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
@ -726,18 +780,19 @@ CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion)
// pass them too, else the DLL interfacing wouldn't be complete and the game possibly wouldn't
// run properly.
plat.bzero (table, sizeof (newgamefuncs_t));
if (!(game.is (GameFlags::Metamod))) {
auto api_GetNewDLLFunctions = game.lib ().resolve <decltype (&GetNewDLLFunctions)> (__func__);
// pass other DLLs engine callbacks to function table...
if (!api_GetNewDLLFunctions || api_GetNewDLLFunctions (&newapi, interfaceVersion) == 0) {
logger.error ("Could not resolve symbol \"%s\" in the game dll.", __func__);
logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __func__);
return HLFalse;
}
dllfuncs.newapi_table = &newapi;
memcpy (table, &newapi, sizeof (newgamefuncs_t));
}
plat.bzero (table, sizeof (newgamefuncs_t));
if (!game.is (GameFlags::Legacy)) {
table->pfnOnFreeEntPrivateData = [] (edict_t *ent) {
@ -847,7 +902,7 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) {
practice.save ();
// disable hooks
util.disableSendTo ();
fakequeries.disable ();
// make sure all stuff cleared
bots.destroy ();
@ -912,7 +967,7 @@ DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *table, globalvars_t *glob) {
// initialize dynamic linkents (no memory hacking with xash3d)
if (!game.is (GameFlags::Xash3D)) {
ents.initialize ();
entlink.initialize ();
}
// give the engine functions to the other DLL...
@ -943,7 +998,7 @@ CR_EXPORT int Server_GetPhysicsInterface (int version, server_physics_api_t *phy
table->version = SV_PHYSICS_INTERFACE_VERSION;
table->SV_CreateEntity = [] (edict_t *ent, const char *name) -> int {
auto func = game.lib ().resolve <EntityFunction> (name); // lookup symbol in game dll
auto func = game.lib ().resolve <EntityProto> (name); // lookup symbol in game dll
// found one in game dll ?
if (func) {
@ -960,85 +1015,6 @@ CR_EXPORT int Server_GetPhysicsInterface (int version, server_physics_api_t *phy
return HLTrue;
}
SharedLibrary::Func EntityLinkage::lookup (SharedLibrary::Handle module, const char *function) {
static const auto &gamedll = game.lib ().handle ();
static const auto &self = m_self.handle ();
const auto resolve = [&] (SharedLibrary::Handle handle) {
return m_dlsym (handle, function);
};
if (ents.needsBypass () && !strcmp (function, "CreateInterface")) {
ents.setPaused (true);
auto ret = resolve (module);
ents.disable ();
return ret;
}
// if requested module is yapb module, put in cache the looked up symbol
if (self != module) {
return resolve (module);
}
#if defined (CR_WINDOWS)
if (HIWORD (function) == 0) {
return resolve (module);
}
#endif
if (m_exports.exists (function)) {
return m_exports[function];
}
auto botAddr = resolve (self);
if (!botAddr) {
auto gameAddr = resolve (gamedll);
if (gameAddr) {
return m_exports[function] = gameAddr;
}
}
else {
return m_exports[function] = botAddr;
}
return nullptr;
}
void EntityLinkage::callPlayerFunction (edict_t *ent) {
EntityFunction playerFunction = nullptr;
if (game.is (GameFlags::Xash3D)) {
playerFunction = game.lib ().resolve <EntityFunction> ("player");
}
else {
playerFunction = reinterpret_cast <EntityFunction> (reinterpret_cast <void *> (lookup (game.lib ().handle (), "player")));
}
if (!playerFunction) {
logger.fatal ("Cannot resolve player () function in gamedll.");
}
else {
playerFunction (&ent->v);
}
}
void EntityLinkage::initialize () {
if (plat.arm || game.is (GameFlags::Metamod)) {
return;
}
m_dlsym.initialize ("kernel32.dll", "GetProcAddress", DLSYM_FUNCTION);
m_dlsym.install (reinterpret_cast <void *> (EntityLinkage::lookupHandler), true);
if (needsBypass ()) {
m_dlclose.initialize ("kernel32.dll", "FreeLibrary", DLCLOSE_FUNCTION);
m_dlclose.install (reinterpret_cast <void *> (EntityLinkage::closeHandler), true);
}
m_self.locate (&engfuncs);
}
// add linkents for android
#include "entities.cpp"

View file

@ -8,7 +8,7 @@
#include <yapb.h>
ConVar cv_autovacate ("autovacate", "1", "Kick bots to automatically make room for human players.");
ConVar cv_autovacate_keep_slots ("autovacate_keep_slots", "1", "How many slots autovacate feature should keep for human players", true, 1.0f, 8.0f);
ConVar cv_autovacate_keep_slots ("autovacate_keep_slots", "1", "How many slots autovacate feature should keep for human players.", true, 1.0f, 8.0f);
ConVar cv_kick_after_player_connect ("kick_after_player_connect", "1", "Kick the bot immediately when a human player joins the server (yb_autovacate must be enabled).");
ConVar cv_quota ("quota", "9", "Specifies the number bots to be added to the game.", true, 0.0f, static_cast <float> (kGameMaxPlayers));
@ -37,6 +37,7 @@ ConVar cv_save_bots_names ("save_bots_names", "1", "Allows to save bot names upo
ConVar cv_botskin_t ("botskin_t", "0", "Specifies the bots wanted skin for Terrorist team.", true, 0.0f, 5.0f);
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);
@ -149,7 +150,15 @@ void BotManager::execGameEntity (edict_t *ent) {
MUTIL_CallGameEntity (PLID, "player", &ent->v);
return;
}
ents.callPlayerFunction (ent);
if (!entlink.callPlayerFunction (ent)) {
for (const auto &bot : m_bots) {
if (bot->ent () == ent) {
bot->kick ();
break;
}
}
}
}
void BotManager::forEach (ForEachBot handler) {
@ -182,25 +191,42 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal
ctrl.msg ("Desired team is stacked. Unable to proceed with bot creation.");
return BotCreateResult::TeamStacked;
}
if (difficulty < 0 || difficulty > 4) {
if (difficulty < Difficulty::Noob || difficulty > Difficulty::Expert) {
difficulty = cv_difficulty.int_ ();
if (difficulty < 0 || difficulty > 4) {
if (difficulty < Difficulty::Noob || difficulty > Difficulty::Expert) {
difficulty = rg.get (3, 4);
cv_difficulty.set (difficulty);
}
}
// try to set proffered personality
static HashMap <String, Personality> personalityMap {
{"normal", Personality::Normal },
{"careful", Personality::Careful },
{"rusher", Personality::Rusher },
};
// set personality if requested
if (personality < Personality::Normal || personality > Personality::Careful) {
if (rg.chance (50)) {
personality = Personality::Normal;
// assign preferred if we're forced with cvar
if (personalityMap.exists (cv_preferred_personality.str ())) {
personality = personalityMap[cv_preferred_personality.str ()];
}
// do a holy random
else {
if (rg.chance (50)) {
personality = Personality::Rusher;
personality = Personality::Normal;
}
else {
personality = Personality::Careful;
if (rg.chance (50)) {
personality = Personality::Rusher;
}
else {
personality = Personality::Careful;
}
}
}
}
@ -685,8 +711,9 @@ bool BotManager::kickRandom (bool decQuota, Team fromTeam) {
// first try to kick the bot that is currently dead
for (const auto &bot : m_bots) {
if (!bot->m_isAlive && belongsTeam (bot.get ())) // is this slot used?
{
// is this slot used?
if (!bot->m_isAlive && belongsTeam (bot.get ())) {
updateQuota ();
bot->kick ();
@ -721,8 +748,9 @@ bool BotManager::kickRandom (bool decQuota, Team fromTeam) {
// worst case, just kick some random bot
for (const auto &bot : m_bots) {
if (belongsTeam (bot.get ())) // is this slot used?
{
// is this slot used?
if (belongsTeam (bot.get ())) {
updateQuota ();
bot->kick ();
@ -824,7 +852,8 @@ void BotManager::listBots () {
};
for (const auto &bot : bots) {
ctrl.msg ("[%-3.1d]\t%-19.16s\t%-10.12s\t%-3.4s\t%-3.1d\t%-3.1d\t%-3.4s\t%-3.0f secs", bot->index (), bot->pev->netname.chars (), bot->m_personality == Personality::Rusher ? "rusher" : bot->m_personality == Personality::Normal ? "normal" : "careful", botTeam (bot->m_team), bot->m_difficulty, static_cast <int> (bot->pev->frags), bot->m_isAlive ? "yes" : "no", cv_rotate_bots.bool_ () ? bot->m_stayTime - game.time () : 0.0f);
auto timelimitStr = cv_rotate_bots.bool_ () ? strings.format ("%-3.0f secs", bot->m_stayTime - game.time ()) : "unlimited";
ctrl.msg ("[%-2.1d]\t%-22.16s\t%-10.12s\t%-3.4s\t%-3.1d\t%-3.1d\t%-3.4s\t%s", bot->index (), bot->pev->netname.chars (), bot->m_personality == Personality::Rusher ? "rusher" : bot->m_personality == Personality::Normal ? "normal" : "careful", botTeam (bot->m_team), bot->m_difficulty, static_cast <int> (bot->pev->frags), bot->m_isAlive ? "yes" : "no", timelimitStr);
}
ctrl.msg ("%d bots", m_bots.length ());
}
@ -1007,7 +1036,9 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
// set all info buffer keys for this bot
auto buffer = engfuncs.pfnGetInfoKeyBuffer (bot);
engfuncs.pfnSetClientKeyValue (clientIndex, buffer, "_vgui_menus", "0");
engfuncs.pfnSetClientKeyValue (clientIndex, buffer, "_ah", "0");
if (!game.is (GameFlags::Legacy)) {
if (cv_show_latency.int_ () == 1) {
@ -1260,7 +1291,16 @@ void BotManager::handleDeath (edict_t *killer, edict_t *victim) {
// notice nearby to victim teammates, that attacker is near
for (const auto &notify : bots) {
if (notify->m_seeEnemyTime + 2.0f < game.time () && notify->m_isAlive && notify->m_team == victimTeam && game.isNullEntity (notify->m_enemy) && killerTeam != victimTeam && util.isVisible (killer->v.origin, notify->ent ())) {
if (notify->m_difficulty >= Difficulty::Hard
&& killerTeam != victimTeam
&& notify->m_seeEnemyTime + 2.0f < game.time ()
&& notify->m_isAlive
&& notify->m_team == victimTeam
&& game.isNullEntity (notify->m_enemy)
&& game.isNullEntity (notify->m_lastEnemy)
&& util.isVisible (killer->v.origin, notify->ent ())) {
// make bot look at last e nemy position
notify->m_actualReactionTime = 0.0f;
notify->m_seeEnemyTime = game.time ();
notify->m_enemy = killer;

View file

@ -1842,7 +1842,6 @@ int Bot::findBombNode () {
return graph.getNearest (bomb, 512.0f); // reliability check
}
int goal = 0, count = 0;
float lastDistanceSq = kInfiniteDistance;

View file

@ -7,10 +7,11 @@
#include <yapb.h>
ConVar cv_path_heuristic_mode ("path_heuristic_mode", "3", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
ConVar cv_path_heuristic_mode ("path_heuristic_mode", "0", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
ConVar cv_path_floyd_memory_limit ("path_floyd_memory_limit", "6", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 32.0f);
ConVar cv_path_dijkstra_simple_distance ("path_dijkstra_simple_distance", "1", "Use simple distance path calculation instead of running full Dijkstra path cycle. Used only when Floyd matrices unavailable due to memory limit.");
ConVar cv_path_astar_post_smooth ("path_astar_post_smooth", "0", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles.");
ConVar cv_path_randomize_on_round_start ("path_randomize_on_round_start", "1", "Randomize pathfinding on each round start.");
float Heuristic::gfunctionKillsDist (int team, int currentIndex, int parentIndex) {
if (parentIndex == kInvalidNodeIndex) {
@ -172,7 +173,7 @@ void AStarAlgo::clearRoute () {
m_routes.clear ();
}
bool AStarAlgo::cantSkipNode (const int a, const int b) {
bool AStarAlgo::cantSkipNode (const int a, const int b, bool skipVisCheck) {
const auto &ag = graph[a];
const auto &bg = graph[b];
@ -181,12 +182,14 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) {
if (hasZeroRadius) {
return true;
}
const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number);
if (notVisible) {
return true;
if (!skipVisCheck) {
const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number);
if (notVisible) {
return true;
}
}
const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f;
if (tooHigh) {
@ -257,6 +260,14 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder
// always clear constructed path
m_constructedPath.clear ();
// round start randomizer offset
auto rsRandomizer = 1.0f;
// randomize path on round start now and then
if (cv_path_randomize_on_round_start.bool_ () && bots.getRoundStartTime () + mp_freezetime.float_ () + 2.0f > game.time ()) {
rsRandomizer = rg.get (0.5f, static_cast <float> (botTeam) * 2.0f + 5.0f);
}
while (!m_routeQue.empty ()) {
// remove the first node from the open list
int currentIndex = m_routeQue.pop ().index;
@ -302,7 +313,7 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder
auto childRoute = &m_routes[child.index];
// calculate the F value as F = G + H
const float g = curRoute->g + m_gcalc (botTeam, child.index, currentIndex);
const float g = curRoute->g + m_gcalc (botTeam, child.index, currentIndex) * rsRandomizer;
const float h = m_hcalc (child.index, kInvalidNodeIndex, destIndex);
const float f = g + h;

View file

@ -32,29 +32,6 @@ BotSupport::BotSupport () {
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 weapon aliases
m_weaponAlias[Weapon::USP] = "usp"; // HK USP .45 Tactical
m_weaponAlias[Weapon::Glock18] = "glock"; // Glock18 Select Fire
@ -112,10 +89,10 @@ bool BotSupport::isVisible (const Vector &origin, edict_t *ent) {
return true;
}
void BotSupport::traceDecals (entvars_t *pev, TraceResult *trace, int logotypeIndex) {
void BotSupport::decalTrace (entvars_t *pev, TraceResult *trace, int logotypeIndex) {
// this function draw spraypaint depending on the tracing results.
auto logo = conf.getRandomLogoName (logotypeIndex);
auto logo = conf.getLogoName (logotypeIndex);
int entityIndex = -1, message = TE_DECAL;
int decalIndex = engfuncs.pfnDecalIndex (logo.chars ());
@ -127,6 +104,7 @@ void BotSupport::traceDecals (entvars_t *pev, TraceResult *trace, int logotypeIn
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);
@ -208,7 +186,6 @@ bool BotSupport::isMonster (edict_t *ent) {
if (isHostageEntity (ent)) {
return false;
}
return true;
}
@ -283,24 +260,30 @@ void BotSupport::checkWelcome () {
if (game.isDedicated () || !cv_display_welcome_text.bool_ () || !m_needToSendWelcome) {
return;
}
m_welcomeReceiveTime = 0.0f;
const bool needToSendMsg = (graph.length () > 0 ? m_needToSendWelcome : true);
auto receiveEntity = game.getLocalEntity ();
auto receiveEnt = game.getLocalEntity ();
if (isAlive (receiveEntity) && m_welcomeReceiveTime < 1.0f && needToSendMsg) {
m_welcomeReceiveTime = game.time () + 4.0f; // receive welcome message in four seconds after game has commencing
if (isAlive (receiveEnt) && m_welcomeReceiveTime < 1.0f && needToSendMsg) {
m_welcomeReceiveTime = game.time () + 2.0f + mp_freezetime.float_ (); // receive welcome message in four seconds after game has commencing
}
if (m_welcomeReceiveTime > 0.0f && needToSendMsg) {
// legacy welcome message, to respect the original code
constexpr StringRef legacyWelcomeMessage = "Welcome to POD-Bot V2.5 by Count Floyd\n"
"Visit http://www.nuclearbox.com/podbot/ or\n"
" http://www.botepidemic.com/podbot for Updates\n";
// it's should be send in very rare cases
const bool sendLegacyWelcome = rg.chance (2);
if (m_welcomeReceiveTime > 0.0f && m_welcomeReceiveTime < game.time () && needToSendMsg) {
if (!game.is (GameFlags::Mobility | GameFlags::Xash3D)) {
game.serverCommand ("speak \"%s\"", m_sentences.random ());
}
String authorStr = "Official Navigation Graph";
StringRef graphAuthor = graph.getAuthor ();
StringRef graphModified = graph.getModifiedBy ();
auto graphAuthor = graph.getAuthor ();
auto graphModified = graph.getModifiedBy ();
if (!graphAuthor.startsWith (product.name)) {
authorStr.assignf ("Navigation Graph by: %s", graphAuthor);
@ -309,30 +292,39 @@ void BotSupport::checkWelcome () {
authorStr.appendf (" (Modified by: %s)", graphModified);
}
}
StringRef modernWelcomeMessage = strings.format ("\nHello! You are playing with %s v%s\nDevised by %s\n\n%s", product.name, product.version, product.author, authorStr);
StringRef modernChatWelcomeMessage = strings.format ("----- %s v%s {%s}, (c) %s, by %s (%s)-----", product.name, product.version, product.date, product.year, product.author, product.url);
MessageWriter (MSG_ONE, msgs.id (NetMsg::TextMsg), nullptr, receiveEntity)
// send a chat-position message
MessageWriter (MSG_ONE, msgs.id (NetMsg::TextMsg), nullptr, receiveEnt)
.writeByte (HUD_PRINTTALK)
.writeString (strings.format ("----- %s v%s {%s}, (c) %s, by %s (%s)-----", product.name, product.version, product.date, product.year, product.author, product.url));
.writeString (modernChatWelcomeMessage.chars ());
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\nDevised by %s\n\n%s", product.name, product.version, product.author, authorStr));
static hudtextparms_t textParams {};
textParams.channel = 1;
textParams.x = -1.0f;
textParams.y = sendLegacyWelcome ? 0.0f : -1.0f;
textParams.effect = rg.get (1, 2);
textParams.r1 = static_cast <uint8_t> (sendLegacyWelcome ? 255 : rg.get (33, 255));
textParams.g1 = static_cast <uint8_t> (sendLegacyWelcome ? 0 : rg.get (33, 255));
textParams.b1 = static_cast <uint8_t> (sendLegacyWelcome ? 0 : rg.get (33, 255));
textParams.a1 = static_cast <uint8_t> (0);
textParams.r2 = static_cast <uint8_t> (sendLegacyWelcome ? 255 : rg.get (230, 255));
textParams.g2 = static_cast <uint8_t> (sendLegacyWelcome ? 255 : rg.get (230, 255));
textParams.b2 = static_cast <uint8_t> (sendLegacyWelcome ? 255 : rg.get (230, 255));
textParams.a2 = static_cast <uint8_t> (200);
textParams.fadeinTime = 0.0078125f;
textParams.fadeoutTime = 2.0f;
textParams.holdTime = 6.0f;
textParams.fxTime = 0.25f;
// send the hud message
game.sendHudMessage (receiveEnt, textParams,
sendLegacyWelcome ? legacyWelcomeMessage.chars () : modernWelcomeMessage.chars ());
m_welcomeReceiveTime = 0.0f;
m_needToSendWelcome = false;
@ -488,10 +480,10 @@ void BotSupport::syncCalculatePings () {
int botPing = bot->m_basePing + rg.get (average.first - part, average.first + part) + rg.get (bot->m_difficulty / 2, bot->m_difficulty);
const int botLoss = rg.get (average.second / 2, average.second);
if (botPing <= 5) {
if (botPing < 2) {
botPing = rg.get (10, 23);
}
else if (botPing > 70) {
else if (botPing > 300) {
botPing = rg.get (30, 40);
}
client.ping = getPingBitmask (client.ent, botLoss, botPing);
@ -522,43 +514,6 @@ void BotSupport::emitPings (edict_t *to) {
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;
}
using SendToHandle = decltype (sendto);
SendToHandle *sendToAddress = sendto;
// linux workaround with sendto
if (!plat.win && !plat.arm) {
SharedLibrary engineLib {};
engineLib.locate (reinterpret_cast <void *> (engfuncs.pfnPrecacheModel));
if (engineLib) {
auto address = engineLib.resolve <SendToHandle *> ("sendto");
if (address != nullptr) {
sendToAddress = address;
}
}
}
m_sendToDetour.initialize ("ws2_32.dll", "sendto", sendToAddress);
// enable only on modern games
if (!game.is (GameFlags::Legacy) && (plat.nix || plat.win) && !plat.arm && !m_sendToDetour.detoured ()) {
m_sendToDetour.install (reinterpret_cast <void *> (BotSupport::sendTo), true);
}
}
bool BotSupport::isModel (const edict_t *ent, StringRef model) {
return model.startsWith (ent->v.model.chars (9));
}
@ -575,58 +530,6 @@ String BotSupport::getCurrentDateTime () {
return String (timebuf);
}
int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) {
const auto send = [&] (const Twin <const uint8_t *, size_t> &msg) -> int32_t {
return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength);
};
auto packet = reinterpret_cast <const uint8_t *> (message);
constexpr int32_t packetLength = 5;
// player replies response
if (length > packetLength && memcmp (packet, "\xff\xff\xff\xff", packetLength - 1) == 0) {
if (packet[4] == 'D') {
QueryBuffer buffer { packet, length, packetLength };
auto count = buffer.read <uint8_t> ();
for (uint8_t i = 0; i < count; ++i) {
buffer.skip <uint8_t> (); // number
auto name = buffer.readString (); // name
buffer.skip <int32_t> (); // score
auto ctime = buffer.read <float> (); // override connection time
buffer.write <float> (bots.getConnectTime (name, ctime));
}
return send (buffer.data ());
}
else if (packet[4] == 'I') {
QueryBuffer buffer { packet, length, packetLength };
buffer.skip <uint8_t> (); // 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_t> (); // players
buffer.skip <uint8_t> (); // maxplayers
buffer.skip <uint8_t> (); // bots
buffer.write <uint8_t> (0); // zero out bot count
return send (buffer.data ());
}
else if (packet[4] == 'm') {
QueryBuffer buffer { packet, length, packetLength };
buffer.shiftToEnd (); // shift to the end of buffer
buffer.write <uint8_t> (0); // zero out bot count
return send (buffer.data ());
}
}
return send ({ packet, length });
}
StringRef BotSupport::weaponIdToAlias (int32_t id) {
StringRef none = "none";
@ -636,7 +539,6 @@ StringRef BotSupport::weaponIdToAlias (int32_t id) {
return none;
}
// helper class for reading wave header
class WaveEndianessHelper final : public NonCopyable {
private:

View file

@ -270,7 +270,7 @@ void Bot::spraypaint_ () {
game.testLine (getEyesPos (), getEyesPos () + forward * 128.0f, TraceIgnore::Monsters, ent (), &tr);
// paint the actual logo decal
util.traceDecals (pev, &tr, m_logotypeIndex);
util.decalTrace (pev, &tr, m_logotypeIndex);
m_timeLogoSpray = game.time () + rg.get (60.0f, 90.0f);
}
}