diff --git a/inc/message.h b/inc/message.h index 23f6a87..73acaae 100644 --- a/inc/message.h +++ b/inc/message.h @@ -32,7 +32,8 @@ CR_DECLARE_SCOPED_ENUM (NetMsg, Fashlight = 21, ItemStatus = 22, ScoreInfo = 23, - ScoreAttrib = 24 + ScoreAttrib = 24, + SayText = 25 ) // vgui menus (since latest steam updates is obsolete, but left for old cs) diff --git a/inc/yapb.h b/inc/yapb.h index bbdf087..0f77f60 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -313,6 +313,7 @@ private: bool m_defuseNotified {}; // bot is notified about bomb defusion bool m_jumpSequence {}; // next path link will be jump link bool m_checkFall {}; // check bot fall + bool m_botMovement {}; // bot movement allowed ? PathWalk m_pathWalk {}; // pointer to current node from path Dodge m_dodgeStrafeDir {}; // direction to strafe @@ -367,6 +368,7 @@ private: CountdownTimer m_approachingLadderTimer {}; // bot is approaching ladder CountdownTimer m_lostReachableNodeTimer {}; // bot's issuing next node, probably he's lost CountdownTimer m_fixFallTimer {}; // timer we're fixed fall last time + CountdownTimer m_repathTimer {}; // bots is going to repath his route private: int pickBestWeapon (Array &vec, int moneySave); @@ -747,6 +749,7 @@ public: void clearTasks (); void dropWeaponForUser (edict_t *user, bool discardC4); void sendToChat (StringRef message, bool teamOnly); + void sendToChatLegacy (StringRef message, bool teamOnly); void pushChatMessage (int type, bool isTeamSay = false); void pushRadioMessage (int message); void pushChatterMessage (int message); diff --git a/src/botlib.cpp b/src/botlib.cpp index e3c03ad..1debb00 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -924,7 +924,7 @@ void Bot::instantChatter (int type) { msg.start (MSG_ONE, msgs.id (NetMsg::SendAudio), nullptr, client.ent); // begin message msg.writeByte (ownIndex); - if (pev->deadflag & DEAD_DYING) { + if (pev->deadflag == DEAD_DYING) { client.iconTimestamp[ownIndex] = game.time () + painSound.duration; writeChatterSound (painSound); } @@ -1696,34 +1696,40 @@ void Bot::overrideConditions () { // then start escape from bomb immediate startTask (Task::EscapeFromBomb, TaskPri::EscapeFromBomb, kInvalidNodeIndex, 0.0f, true); } - constexpr float kReachEnemyWikKnifeDistanceSq = cr::sqrf (102.0f); + float reachEnemyWikKnifeDistanceSq = cr::sqrf (128.0f); // special handling, if we have a knife in our hands if (isKnifeMode () && (util.isPlayer (m_enemy) || (cv_attack_monsters && util.isMonster (m_enemy)))) { - const float distanceSq2d = pev->origin.distanceSq2d (m_enemy->v.origin); + const auto distanceSq2d = pev->origin.distanceSq2d (m_enemy->v.origin); + const auto nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin); + + if (nearestToEnemyPoint != kInvalidNodeIndex && nearestToEnemyPoint != m_currentNodeIndex) { + reachEnemyWikKnifeDistanceSq = graph[nearestToEnemyPoint].origin.distanceSq (m_enemy->v.origin); + reachEnemyWikKnifeDistanceSq += cr::sqrf (48.0f); + } // do nodes movement if enemy is not reachable with a knife - if (distanceSq2d > kReachEnemyWikKnifeDistanceSq && (m_states & Sense::SeeingEnemy)) { - const int nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin); - + if (distanceSq2d > reachEnemyWikKnifeDistanceSq && (m_states & Sense::SeeingEnemy)) { if (nearestToEnemyPoint != kInvalidNodeIndex && nearestToEnemyPoint != m_currentNodeIndex && cr::abs (graph[nearestToEnemyPoint].origin.z - m_enemy->v.origin.z) < 16.0f) { + const float taskTime = game.time () + distanceSq2d / cr::sqrf (m_moveSpeed) * 2.0f; + if (tid != Task::MoveToPosition && !cr::fequal (getTask ()->desire, TaskPri::Hide)) { - startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, game.time () + distanceSq2d / cr::sqrf (m_moveSpeed) * 2.0f, true); + startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, taskTime, true); } else { if (tid == Task::MoveToPosition && getTask ()->data != nearestToEnemyPoint) { clearTask (Task::MoveToPosition); - startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, game.time () + distanceSq2d / cr::sqrf (m_moveSpeed) * 2.0f, true); + startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, taskTime, true); } } } } else { if (!m_isCreature - && distanceSq2d <= kReachEnemyWikKnifeDistanceSq + && distanceSq2d <= reachEnemyWikKnifeDistanceSq && (m_states & Sense::SeeingEnemy) && tid == Task::MoveToPosition) { @@ -2364,7 +2370,7 @@ bool Bot::reactOnEnemy () { if (m_isCreature && !game.isNullEntity (m_enemy)) { m_isEnemyReachable = false; - if (pev->origin.distanceSq (m_enemy->v.origin) < cr::sqrf (128.0f)) { + if (pev->origin.distanceSq2d (m_enemy->v.origin) < cr::sqrf (118.0f)) { m_navTimeset = game.time (); m_isEnemyReachable = true; } @@ -2971,7 +2977,9 @@ void Bot::frame () { // run bot command on twice speed if (m_commandDelay.time <= timestamp) { - runMovement (); + if (m_botMovement || pev->deadflag == DEAD_DYING) { + runMovement (); + } m_commandDelay.time = timestamp + m_commandDelay.interval; } @@ -3036,7 +3044,7 @@ void Bot::update () { m_isCreature = isCreature (); // is bot movement enabled - bool botMovement = false; + m_botMovement = false; // for some unknown reason some bots have speed of 1.0 after respawn on csdm if (game.is (GameFlags::CSDM) && cr::fequal (pev->maxspeed, 1.0f) && tid == Task::Normal) { @@ -3077,17 +3085,17 @@ void Bot::update () { && !cv_freeze_bots && !graph.hasChanged ()) { - botMovement = true; + m_botMovement = true; } checkMsgQueue (); - if (!m_isStale && botMovement) { + if (!m_isStale && m_botMovement) { logic (); // execute main code } else if (pev->maxspeed < 10.0f) { logicDuringFreezetime (); } - else if (!botMovement) { + else if (!m_botMovement) { resetMovement (); } } @@ -4278,6 +4286,7 @@ float Bot::getShiftSpeed () { if (getCurrentTaskId () == Task::SeekCover || (m_aimFlags & AimFlags::Enemy) || isDucking () + || m_isCreature || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK) || (m_currentTravelFlags & PathFlag::Jump) diff --git a/src/chatlib.cpp b/src/chatlib.cpp index 926affe..7539125 100644 --- a/src/chatlib.cpp +++ b/src/chatlib.cpp @@ -249,8 +249,14 @@ void Bot::prepareChatMessage (StringRef message) { size_t replaceCounter = 0; while (replaceCounter < 6 && (pos = m_chatBuffer.find ('%')) != String::InvalidIndex) { + const auto replacePosition = pos + 1; + + if (replacePosition > m_chatBuffer.length ()) { + continue; + } + // found one, let's do replace - switch (m_chatBuffer[pos + 1]) { + switch (m_chatBuffer[replacePosition]) { // the highest frag player case 'f': @@ -294,6 +300,7 @@ void Bot::prepareChatMessage (StringRef message) { case 'g': m_chatBuffer.replace ("%g", graph.getAuthor ()); + break; }; ++replaceCounter; } @@ -375,11 +382,97 @@ void Bot::checkForChat () { } } + + void Bot::sendToChat (StringRef message, bool teamOnly) { // this function prints saytext message to all players if (m_isCreature || message.empty () || !cv_chat) { return; } - issueCommand ("%s \"%s\"", teamOnly ? "say_team" : "say", message); + + // special handling for legacy games + if (game.is (GameFlags::Legacy)) { + sendToChatLegacy (message, teamOnly); + } + else { + issueCommand ("%s \"%s\"", teamOnly ? "say_team" : "say", message); + } +} + +void Bot::sendToChatLegacy (StringRef message, bool teamOnly) { + // this function prints saytext message to all players for legacy games (< cs 1.6) + + // note: for some reason using regular say & say_team for sending chat messages on hlds on a legacy games + // causes buffer overruns somewhere in gamedll Host_Say function, thus crashing the game randomly. + // so this function mimics what legacy gamedll is doing in their Host_Say. + + bool dedicatedSend = false; + + auto sendChatMsg = [&] (const Client &client, String chatMsg) { + auto rcv = bots[client.ent]; + + if (rcv != nullptr) { + rcv->m_sayTextBuffer.entityIndex = m_index; + + rcv->m_sayTextBuffer.sayText = message; + rcv->m_sayTextBuffer.timeNextChat = game.time () + rcv->m_sayTextBuffer.chatDelay; + } + + if (((client.flags & ClientFlags::Alive) && m_isAlive) + || (!(client.flags & ClientFlags::Alive) && m_isAlive) + || (!(client.flags & ClientFlags::Alive) && !m_isAlive)) { + + MessageWriter (MSG_ONE, msgs.id (NetMsg::SayText), nullptr, client.ent) + .writeByte (m_index) + .writeString (chatMsg.chars ()); + } + + if (game.isDedicated () && !dedicatedSend) { + game.print ("%s", chatMsg.trim ()); + } + dedicatedSend = true; + }; + + if (teamOnly) { + StringRef teamName {}; + + if (m_team == Team::Terrorist) { + teamName = "(Terrorist)"; + } + else if (m_team == Team::CT) { + teamName = "(Counter-Terrorist)"; + } + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used) || client.team2 != m_team || client.ent == ent ()) { + continue; + } + String chatMsg {}; + + if (m_isAlive) { + chatMsg.appendf ("%c%s %c%s%c : %s\n", 0x01, teamName, 0x03, pev->netname.chars (), 0x01, message); + } + else { + chatMsg.appendf ("%c*DEAD*%s %c%s%c : %s\n", 0x01, teamName, 0x03, pev->netname.chars (), 0x01, message); + } + sendChatMsg (client, chatMsg); + } + return; + } + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used) || client.ent == ent ()) { + continue; + } + String chatMsg {}; + + if (m_isAlive) { + chatMsg.appendf ("%c%s : %s\n", 0x02, pev->netname.chars (), message); + } + else { + chatMsg.appendf ("%c*DEAD* %c%s%c : %s\n", 0x01, 0x03, pev->netname.chars (), 0x01, message); + } + sendChatMsg (client, chatMsg); + } } diff --git a/src/combat.cpp b/src/combat.cpp index 91aef65..a3a3606 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -1520,6 +1520,10 @@ void Bot::attackMovement () { if (usesKnife () && isEnemyCone) { m_fightStyle = Fight::Strafe; + + if (distanceSq > cr::sqrf (100.0f)) { + m_fightStyle = Fight::None; + } } if (m_fightStyle == Fight::Strafe) { @@ -1584,7 +1588,7 @@ void Bot::attackMovement () { } // do not move if inside "corridor" - if (wallOnRight && wallOnLeft) { + if (wallOnRight && wallOnLeft && !usesKnife ()) { m_strafeSpeed = 0.0f; m_moveSpeed = 0.0f; diff --git a/src/control.cpp b/src/control.cpp index 5723fce..5e3ebc1 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -510,6 +510,14 @@ int BotControl::cmdNodeOn () { mp_roundtime.set (9); mp_freezetime.set (0); mp_timelimit.set (0); + + if (game.is (GameFlags::ReGameDLL)) { + ConVarRef mp_round_infinite ("mp_round_infinite"); + + if (mp_round_infinite.exists ()) { + mp_round_infinite.set ("1"); + } + } } return BotCommandResult::Handled; } @@ -527,6 +535,13 @@ int BotControl::cmdNodeOff () { mp_freezetime.set (m_graphSaveVarValues.freezetime); mp_timelimit.set (m_graphSaveVarValues.timelimit); + if (game.is (GameFlags::ReGameDLL)) { + ConVarRef mp_round_infinite ("mp_round_infinite"); + + if (mp_round_infinite.exists ()) { + mp_round_infinite.set ("0"); + } + } msg ("Graph editor has been disabled."); } else if (arg (option) == "models") { diff --git a/src/manager.cpp b/src/manager.cpp index ec8a7a5..cab0f8c 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1409,6 +1409,8 @@ void BotManager::handleDeath (edict_t *killer, edict_t *victim) { // mark bot as "spawned", and reset it to new-round state when it dead (for csdm/zombie only) if (victimBot != nullptr) { victimBot->spawned (); + + victimBot->m_isAlive = false; } // is this message about a bot who killed somebody? @@ -1559,6 +1561,7 @@ void Bot::newRound () { m_forgetLastVictimTimer.invalidate (); m_lostReachableNodeTimer.invalidate (); m_fixFallTimer.invalidate (); + m_repathTimer.invalidate (); for (auto &timer : m_chatterTimes) { timer = kMaxChatterRepeatInterval; diff --git a/src/message.cpp b/src/message.cpp index 8aab32a..552704a 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -465,6 +465,7 @@ MessageDispatcher::MessageDispatcher () { // we're need next messages IDs but we're won't handle them, so they will be removed from wanted list as soon as they get engine IDs addWanted ("BotVoice", NetMsg::BotVoice, nullptr); addWanted ("SendAudio", NetMsg::SendAudio, nullptr); + addWanted ("SayText", NetMsg::SayText, nullptr); // register text msg cache m_textMsgCache["#CTs_Win"] = TextMsgCache::NeedHandle | TextMsgCache::CounterWin; diff --git a/src/navigate.cpp b/src/navigate.cpp index 170af60..4b97da7 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -476,8 +476,6 @@ void Bot::doPlayerAvoidance (const Vector &normal) { m_hindrance = nullptr; float distanceSq = cr::sqrf (pev->maxspeed); - const auto ownPrio = bots.getPlayerPriority (ent ()); - auto clearCamp = [&] (edict_t *ent) { auto bot = bots[ent]; @@ -496,6 +494,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) { } } }; + const auto ownPrio = bots.getPlayerPriority (ent ()); // find nearest player to bot for (const auto &client : util.getClients ()) { @@ -532,6 +531,27 @@ void Bot::doPlayerAvoidance (const Vector &normal) { if (game.isNullEntity (m_hindrance)) { return; } + + // if we're stuck with a hindrance, probably we're in bad place, find path to single goal for both bots + if (m_isStuck) { + auto other = bots[m_hindrance]; + + if (other != nullptr) { + m_prevGoalIndex = other->m_prevGoalIndex; + m_chosenGoalIndex = other->m_chosenGoalIndex; + + auto destIndex = m_chosenGoalIndex; + + if (!graph.exists (destIndex)) { + destIndex = m_prevGoalIndex; + } + + if (graph.exists (destIndex)) { + findPath (m_currentNodeIndex, destIndex, other->m_pathType); + other->findPath (m_currentNodeIndex, destIndex); + } + } + } const float interval = m_frameInterval * (!isDucking () && pev->velocity.lengthSq2d () > 0.0f ? 6.0f : 2.0f); // use our movement angles, try to predict where we should be next frame @@ -576,8 +596,11 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) { const auto tid = getCurrentTaskId (); + // minimal speed for consider stuck + const float minimalSpeed = isDucking () ? kMinMovedDistance : kMinMovedDistance * 4; + // standing still, no need to check? - if ((m_moveSpeed >= 10 || m_strafeSpeed >= 10) + if ((cr::abs (m_moveSpeed) >= minimalSpeed || cr::abs (m_strafeSpeed) >= minimalSpeed) && m_lastCollTime < game.time () && tid != Task::Attack && tid != Task::Camp) { @@ -1327,6 +1350,11 @@ bool Bot::updateNavigation () { desiredDistanceSq = 0.0f; } + // if just recalculated path, assume reached current node + if (!m_repathTimer.elapsed () && !pathHasFlags) { + desiredDistanceSq = cr::sqrf (72.0f); + } + // needs precise placement - check if we get past the point if (desiredDistanceSq < cr::sqrf (20.0f) && nodeDistanceSq < cr::sqrf (30.0f)) { const auto predictRangeSq = m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval); @@ -3492,6 +3520,7 @@ void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) { } clearSearchNodes (); + m_repathTimer.start (0.5f); m_chosenGoalIndex = srcIndex; m_goalValue = 0.0f; diff --git a/src/tasks.cpp b/src/tasks.cpp index a54412d..8cfb114 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -793,7 +793,7 @@ void Bot::moveToPos_ () { } auto ensureDestIndexOK = [&] (int &index) { - if (isOccupiedNode (index)) { + if (!m_position.empty () && isOccupiedNode (index)) { index = findDefendNode (m_position); } };