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:
parent
01046f7c9a
commit
bf91ef2831
35 changed files with 1256 additions and 734 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); \
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
179
src/hooks.cpp
Normal 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);
|
||||
}
|
||||
160
src/linkage.cpp
160
src/linkage.cpp
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ¬ify : 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;
|
||||
|
|
|
|||
|
|
@ -1842,7 +1842,6 @@ int Bot::findBombNode () {
|
|||
return graph.getNearest (bomb, 512.0f); // reliability check
|
||||
}
|
||||
|
||||
|
||||
int goal = 0, count = 0;
|
||||
float lastDistanceSq = kInfiniteDistance;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
196
src/support.cpp
196
src/support.cpp
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue