From 60036193e5975a62943f61d7d2d04b780bcd5759 Mon Sep 17 00:00:00 2001 From: Angry Bishop <183974626+Angry-Bishop@users.noreply.github.com> Date: Sun, 20 Sep 2026 02:39:40 -0700 Subject: [PATCH 1/2] Improve bot stability and teammate support --- bot.cpp | 333 ++++++++++++++++++++++++++++++++---------- bot.h | 21 +++ bot_client.cpp | 76 ++++++---- bot_combat.cpp | 201 ++++++++++++++++--------- bot_compress.h | 25 +++- bot_ga.cpp | 21 ++- bot_job_assessors.cpp | 65 ++++++++- bot_job_functions.cpp | 70 +++++++-- bot_job_think.cpp | 8 +- bot_neuralnet.cpp | 16 +- dll.cpp | 235 +++++++++++++++++++---------- engine.cpp | 143 +++++++----------- list.h | 3 + util.cpp | 52 ++++--- waypoint.cpp | 212 ++++++++++++++++++--------- 15 files changed, 1021 insertions(+), 460 deletions(-) diff --git a/bot.cpp b/bot.cpp index 905d224..8136ed1 100644 --- a/bot.cpp +++ b/bot.cpp @@ -88,6 +88,14 @@ extern int team_class_limits[4]; extern int spawnAreaWP[4]; // used for tracking the areas where each team spawns extern int max_teams; +// Centralize bounds checks for ally masks. Shifting by -1 (an +// unassigned/spectator entity) is undefined, and team_allies has four rows. +static bool TeamsAreAllied(const int team, const int otherTeam) { + return team >= 0 && team < MAX_TEAMS && + otherTeam >= 0 && otherTeam < MAX_TEAMS && + (team_allies[team] & (1 << otherTeam)) != 0; +} + extern bot_weapon_t weapon_defs[MAX_WEAPONS]; // extern int flf_bug_fix; @@ -123,6 +131,8 @@ extern bool bot_xmas; extern bool g_bot_debug; extern int spectate_debug; // spectators can trigger debug messages from bots extern int bot_bhop; +extern int bot_flag_toss; +extern int bot_flag_toss_distance; extern edict_t* clients[32]; @@ -195,6 +205,7 @@ static int guessThreatLevel(const bot_t* pBot); static void BotReportMyFlagDrop(bot_t* pBot); static void BotEnemyCarrierAlert(bot_t* pBot); static void BotSenseEnvironment(bot_t* pBot); +static void BotFlagTossCheck(bot_t* pBot); static void BotFight(bot_t* pBot); static void BotSpectatorDebug(bot_t* pBot); @@ -295,6 +306,11 @@ void BotSpawnInit(bot_t* pBot) { pBot->job[pBot->currentJob].phase = 0; pBot->f_find_item_time = 0.0f; + pBot->f_flag_toss_time = 0.0f; + pBot->flag_toss_target_index = 0; + pBot->f_support_request_time = 0.0f; + pBot->support_requester_index = 0; + pBot->f_support_commit_time = 0.0f; pBot->strafe_mod = STRAFE_MOD_NORMAL; @@ -736,7 +752,7 @@ void BotCreate(edict_t* pPlayer, const char* arg1, const char* arg2, const char* if (*safe_arg2 != 0) { std::strncpy(c_name, safe_arg2, BOT_NAME_LEN - 1); - c_name[BOT_NAME_LEN] = 0; // make sure c_name is null terminated + c_name[BOT_NAME_LEN - 1] = '\0'; } else { if (number_names > 0) @@ -744,7 +760,7 @@ void BotCreate(edict_t* pPlayer, const char* arg1, const char* arg2, const char* else { // copy the name of the model to the bot's name... std::strncpy(c_name, safe_arg1, BOT_NAME_LEN - 1); - c_name[BOT_NAME_LEN] = '\0'; // make sure c_skin is null terminated + c_name[BOT_NAME_LEN - 1] = '\0'; } } //} @@ -760,7 +776,7 @@ void BotCreate(edict_t* pPlayer, const char* arg1, const char* arg2, const char* else { if (*safe_arg3 != 0) { std::strncpy(c_name, safe_arg3, BOT_NAME_LEN - 1); - c_name[BOT_NAME_LEN] = '\0'; // make sure c_name is null terminated + c_name[BOT_NAME_LEN - 1] = '\0'; } else { if (number_names > 0) @@ -922,6 +938,7 @@ void BotCreate(edict_t* pPlayer, const char* arg1, const char* arg2, const char* pBot->lastFrameHealth = 70; pBot->f_injured_time = 0.0f; + pBot->f_support_commit_time = 0.0f; pBot->deathsTillClassChange = 4; //(int)random_long(4, 15); // time to set up the bots personality stuff @@ -1041,10 +1058,11 @@ void BotFindItem(bot_t* pBot) { edict_t* pent = nullptr; while ((pent = FIND_ENTITY_IN_SPHERE(pent, searchCenter, radius)) != nullptr && !FNullEnt(pent)) { - char item_name[40]; can_pickup = false; // assume can't use it until known otherwise - std::strcpy(item_name, STRING(pent->v.classname)); + // Classnames are engine-owned strings; use them directly instead of + // copying a map-provided value into a fixed local buffer. + const char* item_name = STRING(pent->v.classname); // see if this is a "func_" type of entity (func_button, etc.)... if (std::strncmp("func_", item_name, 5) == 0) { @@ -1342,7 +1360,7 @@ void BotFindItem(bot_t* pBot) { // if it's an enemy teleporter maybe set up a job to attack it const int TeleportTeam = BotTeamColorCheck(pent); - if (pBot->current_team != TeleportTeam && !(team_allies[pBot->current_team] & 1 << TeleportTeam)) { + if (pBot->current_team != TeleportTeam && !TeamsAreAllied(pBot->current_team, TeleportTeam)) { newJob = InitialiseNewJob(pBot, JOB_ATTACK_TELEPORT); if (newJob != nullptr) { newJob->object = pent; @@ -1680,7 +1698,7 @@ edict_t* BotContactThink(bot_t* pBot) { if (vang.y < 0.0f) vang.y += 360.0f; - if (!((UTIL_GetTeamColor(pBot->pEdict) == UTIL_GetTeamColor(pPlayer) || team_allies[pBot->current_team] & 1 << UTIL_GetTeam(pPlayer)) && pPlayer == pBot->enemy.ptr)) { + if (!((UTIL_GetTeamColor(pBot->pEdict) == UTIL_GetTeamColor(pPlayer) || TeamsAreAllied(pBot->current_team, UTIL_GetTeam(pPlayer))) && pPlayer == pBot->enemy.ptr)) { Vector vecEnd = pPlayer->v.origin + pPlayer->v.view_ofs; if (FInViewCone(vecEnd, pBot->pEdict) && FVisible(vecEnd, pBot->pEdict)) { @@ -1774,7 +1792,7 @@ void script(const char* sz) { if (g_bot_debug) { char msg[255]; snprintf(msg, 250, "msg (%s)\n", sz); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); fp = UTIL_OpenFoxbotLog(); if (fp != nullptr) { @@ -1797,7 +1815,7 @@ void script(const char* sz) { if (g_bot_debug) { char msg[255]; snprintf(msg, 250, "no if : %s +++ %s %d\n", msg_msg[current_msg], sz, current_msg); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); /*{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,msg,sz); std::fclose(fp); }*/ } @@ -1965,7 +1983,7 @@ void script(const char* sz) { if (g_bot_debug) { char msg[255]; snprintf(msg, 250, "if : %s +++ %s %d \nComparing point %s\n", msg_msg[current_msg], sz, current_msg, curr->ifs + 4); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); /*{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,msg,sz); std::fclose(fp);}*/ } @@ -1973,7 +1991,7 @@ void script(const char* sz) { if (g_bot_debug) { char msg[64]; snprintf(msg, 63, "Executing if\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); /*{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,msg,sz); std::fclose(fp); }*/ } @@ -2028,8 +2046,18 @@ void script(const char* sz) { int PlayerArmorPercent(const edict_t* pEdict) { const static int tfc_max_armor[10] = { 1, 50, 50, 200, 120, 100, 300, 150, 100, 50 }; - if (mod_id == TFC_DLL && pEdict->v.playerclass >= 0 && pEdict->v.playerclass <= 9) - return static_cast(pEdict->v.armorvalue) / tfc_max_armor[pEdict->v.playerclass] * 100; + if (mod_id == TFC_DLL && pEdict->v.playerclass >= 0 && pEdict->v.playerclass <= 9) { + // Calculate before truncating. The former integer division + // returned 0 for almost every partially armored player and 100 only at full. + int armorPercent = static_cast(pEdict->v.armorvalue * 100.0f / + tfc_max_armor[pEdict->v.playerclass]); + if (armorPercent < 0) + armorPercent = 0; + else if (armorPercent > 100) + armorPercent = 100; + + return armorPercent; + } // Unknown mod, return 100% return 100; @@ -2197,7 +2225,7 @@ static void BotAttackerCheck(bot_t* pBot) { const int player_team = UTIL_GetTeam(pPlayer); // don't target your teammates or allies - if (pBot->current_team == player_team || team_allies[pBot->current_team] & 1 << player_team) + if (pBot->current_team == player_team || TeamsAreAllied(pBot->current_team, player_team)) continue; } @@ -2266,6 +2294,33 @@ static void BotAttackerCheck(bot_t* pBot) { } } +// Create or retarget an urgent support job for a visible saveme +// caller. Medic and Engineer support share the same request lifetime and job. +static bool SubmitUrgentSupportJob(bot_t* pSupportBot, edict_t* pPatient, const int urgentPriority) { + pSupportBot->f_support_request_time = pSupportBot->f_think_time + SUPPORT_REQUEST_DURATION; + pSupportBot->support_requester_index = ENTINDEX(pPatient); + + const int existingJob = BufferedJobIndex(pSupportBot, JOB_BUFF_ALLY); + if (existingJob != -1) { + pSupportBot->job[existingJob].f_bufferedTime = pSupportBot->f_think_time; + pSupportBot->job[existingJob].priority = urgentPriority; + pSupportBot->job[existingJob].phase = 0; + pSupportBot->job[existingJob].phase_timer = 0.0f; + pSupportBot->job[existingJob].waypoint = -1; + pSupportBot->job[existingJob].player = pPatient; + pSupportBot->job[existingJob].origin = pPatient->v.origin; + return true; + } + + job_struct* newJob = InitialiseNewJob(pSupportBot, JOB_BUFF_ALLY); + if (newJob == nullptr) + return false; + + newJob->player = pPatient; + newJob->origin = pPatient->v.origin; + return SubmitNewJob(pSupportBot, JOB_BUFF_ALLY, newJob); +} + // Called by the Sound Hooking Code (in EMIT_SOUND) // This function is going to handle all the sounds that bots can respond to. // We are going to loop through all the bots when we receive a sound, @@ -2346,20 +2401,27 @@ void BotSoundSense(edict_t* pEdict, const char* pszSample, const float fVolume) int nearestMedic = -1; int nearestEngy = -1; - // find the nearest Medic and Engineer to the sound source - for (int index = 0; index < MAX_BOTS; index++) { - // look for the nearest free Medic - if (bots[index].is_used && bots[index].pEdict->v.playerclass == TFC_CLASS_MEDIC && bots[index].enemy.ptr == nullptr) { + // Find the nearest free same-team Medic/Engineer. The old + // loop could select a closer enemy class and only reject it afterward, + // preventing a valid teammate farther down the list from responding. + for (int index = 0; index < MAX_BOTS; index++) { + if (!bots[index].is_used || bots[index].current_team != sourceTeam) + continue; + + // A distant visible enemy no longer makes a Medic + // unavailable. Only close combat or very recent damage prevents an + // urgent response to a nearby saveme call. + if (bots[index].pEdict->v.playerclass == TFC_CLASS_MEDIC && + !MedicHasImmediateCombatThreat(&bots[index])) { botDistance = (bots[index].pEdict->v.origin - pEdict->v.origin).Length(); if (botDistance < hearingDistance && botDistance < nearestMedDist) { nearestMedic = index; nearestMedDist = botDistance; - continue; } } // look for the nearest free Engineer - if (bots[index].is_used && bots[index].pEdict->v.playerclass == TFC_CLASS_ENGINEER && bots[index].enemy.ptr == nullptr && bots[index].m_rgAmmo[weapon_defs[TF_WEAPON_SPANNER].iAmmo1] > 20) { + if (bots[index].pEdict->v.playerclass == TFC_CLASS_ENGINEER && bots[index].enemy.ptr == nullptr && bots[index].m_rgAmmo[weapon_defs[TF_WEAPON_SPANNER].iAmmo1] > 20) { botDistance = (bots[index].pEdict->v.origin - pEdict->v.origin).Length(); if (botDistance < hearingDistance && botDistance < nearestEngDist) { nearestEngy = index; @@ -2370,23 +2432,19 @@ void BotSoundSense(edict_t* pEdict, const char* pszSample, const float fVolume) // send the nearest medic found if (nearestMedic != -1) { - // target the patient if they're visible - if (FInViewCone(pEdict->v.origin, bots[nearestMedic].pEdict) && FVisible(pEdict->v.origin, bots[nearestMedic].pEdict)) { - if (bots[nearestMedic].current_team == UTIL_GetTeam(pEdict)) { - // set up a job to handle the healing/repairing - newJob = InitialiseNewJob(&bots[nearestMedic], JOB_BUFF_ALLY); - if (newJob != nullptr) { - newJob->player = pEdict; - newJob->origin = pEdict->v.origin; // remember where the player was - SubmitNewJob(&bots[nearestMedic], JOB_BUFF_ALLY, newJob); - } - } + bot_t* medic = &bots[nearestMedic]; + // A request becomes urgent only inside the support discovery range. + // Farther calls retain the old investigation response without pulling + // the Medic across the map at healing priority. + if (nearestMedDist <= SUPPORT_DISCOVERY_RANGE && + FVisible(pEdict->v.origin + pEdict->v.view_ofs, medic->pEdict)) { + SubmitUrgentSupportJob(medic, pEdict, MEDIC_HEAL_PRIORITY_URGENT); } else // go find the person who called for a Medic { - newJob = InitialiseNewJob(&bots[nearestMedic], JOB_INVESTIGATE_AREA); + newJob = InitialiseNewJob(medic, JOB_INVESTIGATE_AREA); if (newJob != nullptr) { - const int closestWPToSound = WaypointFindNearest_V(pEdict->v.origin, 500.0f, bots[nearestMedic].current_team); + const int closestWPToSound = WaypointFindNearest_V(pEdict->v.origin, 500.0f, medic->current_team); if (closestWPToSound != -1) { // un-comment this WaypointDrawBeam to see this function in action @@ -2395,31 +2453,26 @@ void BotSoundSense(edict_t* pEdict, const char* pszSample, const float fVolume) // set up a job to investigate the sound newJob->waypoint = closestWPToSound; - SubmitNewJob(&bots[nearestMedic], JOB_INVESTIGATE_AREA, newJob); + SubmitNewJob(medic, JOB_INVESTIGATE_AREA, newJob); } } } } // send the nearest Engineer found - if (nearestEngy != -1) { - // target the patient if they're visible - if (FInViewCone(pEdict->v.origin, bots[nearestEngy].pEdict) && FVisible(pEdict->v.origin, bots[nearestEngy].pEdict)) { - if (bots[nearestEngy].current_team == UTIL_GetTeam(pEdict) && !PlayerIsInfected(pEdict) && PlayerArmorPercent(pEdict) < 100) { - // set up a job to handle the healing/repairing - newJob = InitialiseNewJob(&bots[nearestEngy], JOB_BUFF_ALLY); - if (newJob != nullptr) { - newJob->player = pEdict; - newJob->origin = pEdict->v.origin; // remember where the player was - SubmitNewJob(&bots[nearestEngy], JOB_BUFF_ALLY, newJob); - } - } + if (nearestEngy != -1 && !PlayerIsInfected(pEdict) && PlayerArmorPercent(pEdict) < 100) { + bot_t* engineer = &bots[nearestEngy]; + // Engineers now respond from any facing direction, but armor repair is + // urgent only for a nearby visible caller. + if (nearestEngDist <= SUPPORT_DISCOVERY_RANGE && + FVisible(pEdict->v.origin + pEdict->v.view_ofs, engineer->pEdict)) { + SubmitUrgentSupportJob(engineer, pEdict, ENGINEER_REPAIR_PRIORITY_URGENT); } else // go find the person who called for a Medic { - newJob = InitialiseNewJob(&bots[nearestEngy], JOB_INVESTIGATE_AREA); + newJob = InitialiseNewJob(engineer, JOB_INVESTIGATE_AREA); if (newJob != nullptr) { - const int closestWPToSound = WaypointFindNearest_V(pEdict->v.origin, 500.0, bots[nearestEngy].current_team); + const int closestWPToSound = WaypointFindNearest_V(pEdict->v.origin, 500.0, engineer->current_team); if (closestWPToSound != -1) { // un-comment this WaypointDrawBeam to see this function in action @@ -2428,7 +2481,7 @@ void BotSoundSense(edict_t* pEdict, const char* pszSample, const float fVolume) // set up a job to investigate the sound newJob->waypoint = closestWPToSound; - SubmitNewJob(&bots[nearestEngy], JOB_INVESTIGATE_AREA, newJob); + SubmitNewJob(engineer, JOB_INVESTIGATE_AREA, newJob); } } } @@ -2693,7 +2746,7 @@ static void BotGrenadeAvoidance(bot_t* pBot) { const int owner_team = UTIL_GetTeam(pent->v.owner); // try to get over or around the pipebomb if an enemy fired it - if (owner_team != pBot->current_team && !(team_allies[pBot->current_team] & 1 << owner_team)) { + if (owner_team != pBot->current_team && !TeamsAreAllied(pBot->current_team, owner_team)) { // UTIL_HostSay(pBot->pEdict, 0, "enemy PIPEBOMB spotted!");//DebugMessageOfDoom! entity_origin = pent->v.origin; @@ -2766,11 +2819,17 @@ static void BotRoleCheck(bot_t* pBot) { for (i = 0; i < MAX_BOTS; i++) { // Create lists of the defender/attacker roles any bots are playing if (bots[i].is_used && bots[i].pEdict->v.playerclass) { - teams.total[bots[i].pEdict->v.team - 1]++; + // Spectators use team 6 and connecting clients can use + // team 0. Never use either value to index the four-team arrays. + const int team = bots[i].pEdict->v.team - 1; + if (team < 0 || team >= MAX_TEAMS) + continue; + + teams.total[team]++; if (bots[i].mission == ROLE_DEFENDER) - teams.defenders[bots[i].pEdict->v.team - 1].addTail(&bots[i]); + teams.defenders[team].addTail(&bots[i]); else if (bots[i].mission == ROLE_ATTACKER) - teams.attackers[bots[i].pEdict->v.team - 1].addTail(&bots[i]); + teams.attackers[team].addTail(&bots[i]); else if (bots[i].mission == ROLE_NONE) bots[i].mission = ROLE_ATTACKER; } @@ -2903,15 +2962,12 @@ static void BotComms(bot_t* pBot) { else std::strcpy(fromName, pBot->message); - int counter = 0; - // Pull the name out of the message. - while (true) { - if (fromName[counter] == ':' && fromName[counter + 1] == ' ') { - fromName[counter] = '\0'; - break; - } - counter++; - } + // Pull the name out only when the expected chat delimiter exists. + // A malformed command must not scan beyond the message buffer. + char* nameEnd = std::strstr(fromName, ": "); + if (nameEnd == nullptr) + continue; + *nameEnd = '\0'; // Get the class from the command line. char theClass = '\0'; // Initialize theClass to a default value @@ -2936,15 +2992,11 @@ static void BotComms(bot_t* pBot) { else std::strcpy(fromName, pBot->message); - int counter = 0; - // Pull the name out of the message. - while (true) { - if (fromName[counter] == ':' && fromName[counter + 1] == ' ') { - fromName[counter] = '\0'; - break; - } - counter++; - } + // Pull the name out only when the expected chat delimiter exists. + char* nameEnd = std::strstr(fromName, ": "); + if (nameEnd == nullptr) + continue; + *nameEnd = '\0'; BotChangeRole(pBot, pBot->message, fromName); } /* if(strcasecmp("follow", cmd) == 0) @@ -3222,7 +3274,7 @@ static bool botVerifyAccess(edict_t *pPlayer) { char szBuffer[64]; snprintf(szBuffer, sizeof(szBuffer), "%s Does not have access.", authId); - ALERT(at_console, szBuffer); + ALERT(at_console, "%s", szBuffer); // example steam ID: STEAM_0:1245 bool found = false; @@ -3541,7 +3593,7 @@ bool SpyAmbushAreaCheck(const bot_t* pBot, Vector& r_wallVector) { // ignore allied players const int player_team = UTIL_GetTeam(pPlayer); - if (player_team > -1 && (player_team == pBot->current_team || team_allies[pBot->current_team] & 1 << player_team)) + if (player_team > -1 && (player_team == pBot->current_team || TeamsAreAllied(pBot->current_team, player_team))) continue; if (VectorsNearerThan(pPlayer->v.origin, pBot->pEdict->v.origin, 1200.0)) { @@ -3768,7 +3820,9 @@ void BotThink(bot_t* pBot) { // keep an up-to-date record of which team the bot is on pBot->current_team = UTIL_GetTeam(pBot->pEdict); - if (pBot->current_team < 0) // shouldn't happen, but, just in case + // Guard both ends before current_team reaches team arrays and + // bit shifts. Spectator team 6 is not a playable bot team. + if (pBot->current_team < 0 || pBot->current_team >= MAX_TEAMS) { pBot->not_started = true; // try joining again return; @@ -3892,6 +3946,10 @@ void BotThink(bot_t* pBot) { BotJobThink(pBot); BotRunJobs(pBot); + // Run after normal jobs so a pending handoff gets the final facing choice + // for this frame. The bot does not throw until it has actually turned. + BotFlagTossCheck(pBot); + if (spectate_debug) BotSpectatorDebug(pBot); @@ -4170,8 +4228,116 @@ static void BotSenseEnvironment(bot_t* pBot) { BotSprayLogo(pBot->pEdict, true); } } - else + else { pBot->bot_has_flag = false; + pBot->flag_toss_target_index = 0; + pBot->f_flag_toss_time = 0.0f; + } +} + +// Test whether a client is still a safe flag-handoff target. +// "Teammate" deliberately means the same team rather than merely an allied +// team, and a player already carrying a GoalItem is not eligible. +static bool BotFlagTossTargetValid(const bot_t* pBot, edict_t* pPlayer) { + if (!pPlayer || pPlayer->free || pPlayer == pBot->pEdict || + (pPlayer->v.flags & FL_FAKECLIENT) || !IsAlive(pPlayer)) + return false; + + if (UTIL_GetTeam(pPlayer) != pBot->current_team || PlayerHasFlag(pPlayer)) + return false; + + if (!VectorsNearerThan(pPlayer->v.origin, pBot->pEdict->v.origin, + static_cast(bot_flag_toss_distance))) + return false; + + return FVisible(pPlayer->v.origin + pPlayer->v.view_ofs, pBot->pEdict); +} + +// Face a nearby human teammate before using TFC's normal +// voluntary GoalItem drop command. Failed drops are retried after 1.5 seconds; +// successful drops are announced only after ownership actually changes. +static void BotFlagTossCheck(bot_t* pBot) { + if (!bot_flag_toss || !pBot->bot_has_flag) { + pBot->flag_toss_target_index = 0; + return; + } + + edict_t* pTarget = nullptr; + if (pBot->flag_toss_target_index > 0 && + pBot->flag_toss_target_index <= std::min(gpGlobals->maxClients, MAX_BOTS)) { + pTarget = INDEXENT(pBot->flag_toss_target_index); + if (!BotFlagTossTargetValid(pBot, pTarget)) { + pTarget = nullptr; + pBot->flag_toss_target_index = 0; + } + } + + // With no active recipient, throttle the full player scan to four times + // per second. f_flag_toss_time also holds the delay after a failed throw. + if (!pTarget) { + if (pBot->f_flag_toss_time > pBot->f_think_time) + return; + + pBot->f_flag_toss_time = pBot->f_think_time + 0.25f; + float nearestDistance = static_cast(bot_flag_toss_distance) + 1.0f; + const int clientLimit = std::min(gpGlobals->maxClients, MAX_BOTS); + + for (int i = 1; i <= clientLimit; ++i) { + edict_t* pPlayer = INDEXENT(i); + if (!BotFlagTossTargetValid(pBot, pPlayer)) + continue; + + const float distance = (pPlayer->v.origin - pBot->pEdict->v.origin).Length(); + if (distance < nearestDistance) { + pTarget = pPlayer; + nearestDistance = distance; + pBot->flag_toss_target_index = i; + } + } + + if (!pTarget) + return; + } + + // Override the navigation/combat facing selected earlier in this frame. + // Waiting for a small yaw error stops dropitems from throwing the flag in + // the direction the bot happened to be looking when the teammate arrived. + BotSetFacing(pBot, pTarget->v.origin + pTarget->v.view_ofs); + float yawError = pBot->pEdict->v.v_angle.y - pBot->pEdict->v.ideal_yaw; + if (yawError < -180.0f) + yawError += 360.0f; + else if (yawError > 180.0f) + yawError -= 360.0f; + + if (fabsf(yawError) > 10.0f || pBot->f_flag_toss_time > pBot->f_think_time) + return; + + const int thrownFlagImpulse = pBot->flag_impulse; + FakeClientCommand(pBot->pEdict, "dropitems", nullptr, nullptr); + + // ClientCommand handling is synchronous. Confirm that the exact GoalItem + // is no longer owned before announcing a successful throw. + bool stillCarrying = false; + edict_t* pGoal = nullptr; + while ((pGoal = FIND_ENTITY_BY_CLASSNAME(pGoal, "item_tfgoal")) != nullptr && !FNullEnt(pGoal)) { + if (pGoal->v.owner == pBot->pEdict && pGoal->v.impulse == thrownFlagImpulse) { + stillCarrying = true; + break; + } + } + + if (!stillCarrying) { + char message[96]; + snprintf(message, sizeof(message), "Flag thrown to %s", STRING(pTarget->v.netname)); + message[sizeof(message) - 1] = '\0'; + UTIL_HostSay(pBot->pEdict, 1, message); + } + + // On failure, release the facing override during the retry delay. A fresh + // scan after 1.5 seconds accommodates movement or newly available space. + pBot->flag_toss_target_index = 0; + pBot->f_flag_toss_time = pBot->f_think_time + 1.5f; + } // This function handles basic combat actions, such as pointing the active @@ -4181,6 +4347,19 @@ static void BotFight(bot_t* pBot) { const edict_t* pEdict = pBot->pEdict; const edict_t* pent = pBot->enemy.ptr; // assign the enemy entity to pent + // Once urgent healing is the selected job, do not let the + // earlier combat pass leave IN_ATTACK set or reselect a gun for a distant + // enemy. JobBuffAlly runs later in the frame and owns facing, movement, and + // medikit use. Immediate close combat or recent damage still interrupts the + // support job in its assessor before this state can persist. + const bool urgentMedicCare = pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC && + pBot->currentJob >= 0 && pBot->currentJob < JOB_BUFFER_MAX && + pBot->jobType[pBot->currentJob] == JOB_BUFF_ALLY && + pBot->job[pBot->currentJob].priority >= MEDIC_HEAL_PRIORITY_URGENT && + !MedicHasImmediateCombatThreat(pBot); + if (urgentMedicCare) + return; + if (pEdict != nullptr && pent != nullptr) { // Calculate the distance between the bot and its enemy const float distance = (pent->v.origin - pEdict->v.origin).Length(); @@ -4228,7 +4407,7 @@ static void BotCombatThink(bot_t* pBot) { pBot->f_duck_time = pBot->f_think_time + 0.3f; // ignore allies - if (pBot->current_team == enemy_team || team_allies[pBot->current_team] & 1 << enemy_team) + if (pBot->current_team == enemy_team || TeamsAreAllied(pBot->current_team, enemy_team)) return; const int ThreatLevel = guessThreatLevel(pBot); @@ -4661,4 +4840,4 @@ static void BotSpectatorDebug(bot_t* pBot) { return; } -} \ No newline at end of file +} diff --git a/bot.h b/bot.h index dbbcbfd..63370bf 100644 --- a/bot.h +++ b/bot.h @@ -234,6 +234,21 @@ typedef struct { // maximum number of jobs storable in the bot's job buffer constexpr int JOB_BUFFER_MAX = 5; +// Medic and Engineer support starts nearby and may finish a short, bounded +// chase. Urgent Medic care can override ordinary distant combat, but close +// combat and recent damage remain immediate threats and interrupt treatment. +constexpr float SUPPORT_DISCOVERY_RANGE = 400.0f; +constexpr float SUPPORT_MAX_CHASE_RANGE = 600.0f; +constexpr float SUPPORT_REQUEST_DURATION = 5.0f; +constexpr float SUPPORT_COMMIT_DURATION = 1.25f; +constexpr float MEDIC_IMMEDIATE_THREAT_RANGE = 250.0f; +constexpr float MEDIC_DAMAGE_INTERRUPT_DURATION = 1.0f; +constexpr int MEDIC_HEAL_PRIORITY_NORMAL = 540; +constexpr int MEDIC_HEAL_PRIORITY_URGENT = 710; +constexpr int ENGINEER_REPAIR_PRIORITY_NORMAL = 525; +constexpr int ENGINEER_REPAIR_PRIORITY_URGENT = 650; +constexpr int ENGINEER_CRITICAL_ARMOR_PERCENT = 25; + // this structure is shared by all the job types in the bot's job buffer typedef struct { float f_bufferedTime; // how long ago this job was put into the buffer @@ -349,6 +364,11 @@ typedef struct { int routeFailureTally; // used to spot successive failures to get to a waypoint goal float f_find_item_time; // when to next check environment for interesting objects + float f_flag_toss_time; // next recipient scan or retry time + int flag_toss_target_index; // 1-based client index; 0 when no handoff is pending + float f_support_request_time; // expiration time for an urgent saveme request + int support_requester_index; // 1-based patient index; 0 when no request is active + float f_support_commit_time; // short Medic commitment/hysteresis window float f_pause_time; // remembers when to unpause the bot float f_duck_time; // remembers when the bot should stop crouching @@ -654,6 +674,7 @@ void script(const char* sz); int PlayerArmorPercent(const edict_t* pEdict); int PlayerHealthPercent(const edict_t* pEdict); +bool MedicHasImmediateCombatThreat(const bot_t* pBot); void UTIL_SavePent(edict_t* pent); diff --git a/bot_client.cpp b/bot_client.cpp index e794a64..cc0311c 100644 --- a/bot_client.cpp +++ b/bot_client.cpp @@ -55,6 +55,37 @@ bot_weapon_t weapon_defs[MAX_WEAPONS]; // array of weapon definitions int g_state; +// Engine messages are external input to these parsers. Keep every +// weapon/ammo value inside the fixed GoldSrc arrays before it is used. +static bool ValidBotIndex(const int index) { + return index >= 0 && index < MAX_BOTS; +} + +static bool ValidWeaponIndex(const int index) { + return index >= 0 && index < MAX_WEAPONS; +} + +static bool ValidAmmoIndex(const int index) { + return index >= 0 && index < MAX_AMMO_SLOTS; +} + +static void UpdateCurrentWeaponAmmo(const int bot_index) { + if (!ValidBotIndex(bot_index)) + return; + + const int weapon = bots[bot_index].current_weapon.iId; + if (!ValidWeaponIndex(weapon)) { + bots[bot_index].current_weapon.iAmmo1 = 0; + bots[bot_index].current_weapon.iAmmo2 = 0; + return; + } + + const int ammo1 = weapon_defs[weapon].iAmmo1; + const int ammo2 = weapon_defs[weapon].iAmmo2; + bots[bot_index].current_weapon.iAmmo1 = ValidAmmoIndex(ammo1) ? bots[bot_index].m_rgAmmo[ammo1] : 0; + bots[bot_index].current_weapon.iAmmo2 = ValidAmmoIndex(ammo2) ? bots[bot_index].m_rgAmmo[ammo2] : 0; +} + // int MatchScores[4]; // doesn't update reliably on all maps // This message is sent when the TFC VGUI menu is displayed. @@ -152,7 +183,8 @@ void BotClient_Valve_WeaponList(void* p, const int bot_index) { if (state == 0) { state++; - std::strcpy(bot_weapon.szClassname, static_cast(p)); + std::strncpy(bot_weapon.szClassname, static_cast(p), sizeof(bot_weapon.szClassname) - 1); + bot_weapon.szClassname[sizeof(bot_weapon.szClassname) - 1] = '\0'; } else if (state == 1) { state++; @@ -187,8 +219,9 @@ void BotClient_Valve_WeaponList(void* p, const int bot_index) { bot_weapon.iFlags = *static_cast(p); // flags for weapon (WTF???) - // store away this weapon with it's ammo information... - weapon_defs[bot_weapon.iId] = bot_weapon; + // Store only IDs that fit the fixed weapon definition table. + if (ValidWeaponIndex(bot_weapon.iId)) + weapon_defs[bot_weapon.iId] = bot_weapon; } } @@ -236,16 +269,14 @@ void BotClient_Valve_CurrentWeapon(void* p, const int bot_index) { iClip = *static_cast(p); // ammo currently in the clip for this weapon - if (iId <= 31) { - bots[bot_index].bot_weapons |= 1 << iId; // set this weapon bit + if (ValidBotIndex(bot_index) && ValidWeaponIndex(iId)) { + bots[bot_index].bot_weapons |= static_cast(1u << iId); // set this weapon bit if (iState == 1) { bots[bot_index].current_weapon.iId = iId; bots[bot_index].current_weapon.iClip = iClip; - // update the ammo counts for this weapon... - bots[bot_index].current_weapon.iAmmo1 = bots[bot_index].m_rgAmmo[weapon_defs[iId].iAmmo1]; - bots[bot_index].current_weapon.iAmmo2 = bots[bot_index].m_rgAmmo[weapon_defs[iId].iAmmo2]; + UpdateCurrentWeaponAmmo(bot_index); } } } @@ -289,13 +320,10 @@ void BotClient_Valve_AmmoX(void* p, const int bot_index) { ammount = *static_cast(p); // the amount of ammo currently available - bots[bot_index].m_rgAmmo[index] = ammount; // store it away - - const int ammo_index = bots[bot_index].current_weapon.iId; - - // update the ammo counts for this weapon... - bots[bot_index].current_weapon.iAmmo1 = bots[bot_index].m_rgAmmo[weapon_defs[ammo_index].iAmmo1]; - bots[bot_index].current_weapon.iAmmo2 = bots[bot_index].m_rgAmmo[weapon_defs[ammo_index].iAmmo2]; + if (ValidBotIndex(bot_index) && ValidAmmoIndex(index)) { + bots[bot_index].m_rgAmmo[index] = ammount; // store it away + UpdateCurrentWeaponAmmo(bot_index); + } } } @@ -340,13 +368,10 @@ void BotClient_Valve_AmmoPickup(void* p, const int bot_index) { ammount = *static_cast(p); - bots[bot_index].m_rgAmmo[index] = ammount; - - const int ammo_index = bots[bot_index].current_weapon.iId; - - // update the ammo counts for this weapon... - bots[bot_index].current_weapon.iAmmo1 = bots[bot_index].m_rgAmmo[weapon_defs[ammo_index].iAmmo1]; - bots[bot_index].current_weapon.iAmmo2 = bots[bot_index].m_rgAmmo[weapon_defs[ammo_index].iAmmo2]; + if (ValidBotIndex(bot_index) && ValidAmmoIndex(index)) { + bots[bot_index].m_rgAmmo[index] = ammount; + UpdateCurrentWeaponAmmo(bot_index); + } } } @@ -377,8 +402,9 @@ void BotClient_FLF_AmmoPickup(void* p, int bot_index) void BotClient_Valve_WeaponPickup(void* p, const int bot_index) { const int index = *static_cast(p); - // set this weapon bit to indicate that we are carrying this weapon - bots[bot_index].bot_weapons |= 1 << index; + // Set this weapon bit only when both fixed-array indices are valid. + if (ValidBotIndex(bot_index) && ValidWeaponIndex(index)) + bots[bot_index].bot_weapons |= static_cast(1u << index); } void BotClient_TFC_WeaponPickup(void* p, const int bot_index) { @@ -1108,4 +1134,4 @@ void BotClient_TFC_Scores(void* p, int bot_index) else if (state == 2) state = 0; } -#endif \ No newline at end of file +#endif diff --git a/bot_combat.cpp b/bot_combat.cpp index 0d6fed9..c0d0335 100644 --- a/bot_combat.cpp +++ b/bot_combat.cpp @@ -70,6 +70,14 @@ static float bot_snipe_max_inaccuracy[5] = {19.0f, 28.0f, 37.0f, 46.0f, 55.0f}; extern bool is_team[4]; extern int team_allies[4]; +// Reject invalid/spectator team values before indexing ally masks +// or shifting a bit by an out-of-range count. +static bool TeamsAreAllied(const int team, const int otherTeam) { + return team >= 0 && team < MAX_TEAMS && + otherTeam >= 0 && otherTeam < MAX_TEAMS && + (team_allies[team] & (1 << otherTeam)) != 0; +} + extern bot_t bots[32]; // list of which player indices carry a flag(updated each frame) @@ -80,6 +88,7 @@ static bool playerHasFlag[32]; // FUNCTION PROTOTYPES /////////////// static void BotPipeBombCheck(const bot_t* pBot); static edict_t* BotFindEnemy(bot_t* pBot); +static void BotMedicPatientCheck(bot_t* pBot); static bool BotSpyDetectCheck(bot_t* pBot, edict_t* pNewEnemy); static void BotSGSpotted(bot_t* pBot, edict_t* sg); static bool BotPrimeGrenade(bot_t* pBot, int slot, unsigned char nadeType, unsigned short reserve); @@ -209,7 +218,7 @@ int FriendlyClassTotal(const edict_t *pEdict, const int specifiedClass, const bo const int player_team = UTIL_GetTeam(pPlayer); // add another if the player is a teammate or ally - if (my_team == player_team || team_allies[my_team] & 1 << player_team) + if (my_team == player_team || TeamsAreAllied(my_team, player_team)) ++classTotal; } } @@ -289,7 +298,7 @@ static void BotFeigningEnemyCheck(bot_t *pBot) { // ignore allied players const int player_team = UTIL_GetTeam(pPlayer); - if (player_team > -1 && (player_team == pBot->current_team || team_allies[pBot->current_team] & 1 << player_team)) + if (player_team > -1 && (player_team == pBot->current_team || TeamsAreAllied(pBot->current_team, player_team))) continue; // is this enemy near and facing away from the bot? @@ -312,6 +321,62 @@ static void BotFeigningEnemyCheck(bot_t *pBot) { pBot->visEnemyCount = 0; } +// Distinguish immediate combat from merely having a visible +// enemy. A close enemy or damage during the last second interrupts healing; +// a distant visible enemy does not automatically invalidate urgent care. +bool MedicHasImmediateCombatThreat(const bot_t* pBot) { + if (pBot->f_injured_time + MEDIC_DAMAGE_INTERRUPT_DURATION > pBot->f_think_time) + return true; + + return pBot->enemy.ptr != nullptr && + pBot->enemy.f_seenDistance <= MEDIC_IMMEDIATE_THREAT_RANGE; +} + +// Discover nearby Medic patients independently of enemy target selection. +// Keeping this outside BotFindEnemy() prevents its current-enemy fast path +// from returning before patient discovery is reached. +static void BotMedicPatientCheck(bot_t* pBot) { + if (mod_id != TFC_DLL || pBot->pEdict->v.playerclass != TFC_CLASS_MEDIC || + MedicHasImmediateCombatThreat(pBot)) + return; + + edict_t* nearestPatient = nullptr; + float nearestPatientDistance = SUPPORT_DISCOVERY_RANGE; + + for (int i = 1; i <= std::min(gpGlobals->maxClients, MAX_BOTS); ++i) { + edict_t* pPlayer = INDEXENT(i); + + if (!pPlayer || pPlayer->free || pPlayer == pBot->pEdict || !IsAlive(pPlayer)) + continue; + + // Support actual same-team players rather than crossing to teams that + // happen to be allied by a map rule. + if (UTIL_GetTeam(pPlayer) != pBot->current_team || pPlayer->v.max_health <= 0.0f) + continue; + + // Any health below the normal maximum is now urgent within 400 units; + // infection remains urgent even when current health is full. + if (pPlayer->v.health >= pPlayer->v.max_health && !PlayerIsInfected(pPlayer)) + continue; + + const float distance = (pPlayer->v.origin - pBot->pEdict->v.origin).Length(); + const Vector patientEyes = pPlayer->v.origin + pPlayer->v.view_ofs; + if (distance <= nearestPatientDistance && FVisible(patientEyes, pBot->pEdict)) { + nearestPatient = pPlayer; + nearestPatientDistance = distance; + } + } + + if (nearestPatient) { + job_struct* newJob = InitialiseNewJob(pBot, JOB_BUFF_ALLY); + if (newJob != nullptr) { + newJob->player = nearestPatient; + newJob->origin = nearestPatient->v.origin; + SubmitNewJob(pBot, JOB_BUFF_ALLY, newJob); + } + } +} + // This function will first check that the bots current enemy is still a // valid target for attacking. // Then it will check if the bot can see a new(perhaps more important) target. @@ -469,7 +534,14 @@ void BotEnemyCheck(bot_t *pBot) { pBot->enemy.f_firstSeen = pBot->f_think_time; pBot->enemy.f_lastSeen = pBot->f_think_time; pBot->enemy.lastLocation = new_enemy->v.origin; + // Keep the distance current immediately so support logic can decide + // whether combat is close enough to interrupt urgent healing. + pBot->enemy.f_seenDistance = (pBot->pEdict->v.origin - new_enemy->v.origin).Length(); } + + // Run after enemy selection so immediate-threat filtering uses the current + // target and distance. This scan remains throttled by f_enemy_check_time. + BotMedicPatientCheck(pBot); } // This function is responsible for checking if the bot can see a new @@ -509,7 +581,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { // don't target your own team's sentry guns... // don't target allied sentry guns either... - if (pBot->current_team == sentry_team || team_allies[pBot->current_team] & 1 << sentry_team) + if (pBot->current_team == sentry_team || TeamsAreAllied(pBot->current_team, sentry_team)) continue; vecEnd = pent->v.origin + pent->v.view_ofs; @@ -532,7 +604,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { continue; int sentry_team = pent->v.team - 1; // don't target friendly sentry guns... - if (pBot->current_team == sentry_team || team_allies[pBot->current_team] & 1 << sentry_team) + if (pBot->current_team == sentry_team || TeamsAreAllied(pBot->current_team, sentry_team)) continue; // ntf_capture_mg 1 = ignore, we can cap it @@ -581,55 +653,47 @@ static edict_t *BotFindEnemy(bot_t *pBot) { nearestDistance = 1000.0f; if (mod_id == TFC_DLL) { - // get medics and engineers to heal/repair teammates - if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC || (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER && pBot->m_rgAmmo[weapon_defs[TF_WEAPON_SPANNER].iAmmo1] > 80)) { - nearestDistance = 1000.0f; - edict_t *pPlayer; - int player_team; - - // search the world for players... - for (i = 1; i <= gpGlobals->maxClients; i++) { - pPlayer = INDEXENT(i); - - // skip invalid players and skip self (i.e. this bot) - if (pPlayer && !pPlayer->free && pPlayer != pEdict) { - // skip this player if they're not alive - if (!IsAlive(pPlayer)) - continue; - - // skip human players in observer mode - if (observer_mode && !(pPlayer->v.flags & FL_FAKECLIENT)) - continue; - - player_team = UTIL_GetTeamColor(pPlayer); - - // ignore all enemies... - if (pBot->current_team != player_team && !(team_allies[pBot->current_team] & 1 << player_team)) - continue; - - // check if the player needs to be healed - if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC && pPlayer->v.health / pPlayer->v.max_health > 0.80f && !PlayerIsInfected(pPlayer)) // scores a point, even to selfish medics - continue; // health greater than 70% so ignore - - // check if the player needs to be armored... - if (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER && (PlayerIsInfected(pPlayer) || PlayerArmorPercent(pPlayer) > 60)) - continue; // armor greater than 60% so ignore - was 50% [APG]RoboCop[CL] - - // see if bot can see the player... - float distance = (pPlayer->v.origin - pEdict->v.origin).Length(); - vecEnd = pPlayer->v.origin + pPlayer->v.view_ofs; - if (distance < nearestDistance && FInViewCone(vecEnd, pEdict) && FVisible(vecEnd, pEdict)) { - nearestDistance = distance; + // Give Engineers the same reliable support discovery used + // by Medics. Select the nearest visible same-team player rather than the + // first client slot, and do not divert from combat to repair armor. + if (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER && + pBot->enemy.ptr == nullptr && + pBot->m_rgAmmo[weapon_defs[TF_WEAPON_SPANNER].iAmmo1] > 80) { + edict_t* nearestPatient = nullptr; + float nearestPatientDistance = SUPPORT_DISCOVERY_RANGE; + + for (i = 1; i <= std::min(gpGlobals->maxClients, MAX_BOTS); ++i) { + edict_t* pPlayer = INDEXENT(i); + + if (!pPlayer || pPlayer->free || pPlayer == pEdict || !IsAlive(pPlayer)) + continue; - // set up a job to handle the healing/repairing - job_struct *newJob = InitialiseNewJob(pBot, JOB_BUFF_ALLY); - if (newJob != nullptr) { - newJob->player = pPlayer; - newJob->origin = pPlayer->v.origin; // remember where the player was seen - SubmitNewJob(pBot, JOB_BUFF_ALLY, newJob); - } - break; - } + if (observer_mode && !(pPlayer->v.flags & FL_FAKECLIENT)) + continue; + + // Repair only the Engineer's actual team, not map-defined allies. + if (UTIL_GetTeam(pPlayer) != pBot->current_team) + continue; + + // Preserve the original automatic-repair threshold, now using the + // corrected armor percentage calculation. + if (PlayerIsInfected(pPlayer) || PlayerArmorPercent(pPlayer) > 60) + continue; + + const float distance = (pPlayer->v.origin - pEdict->v.origin).Length(); + const Vector patientEyes = pPlayer->v.origin + pPlayer->v.view_ofs; + if (distance <= nearestPatientDistance && FVisible(patientEyes, pEdict)) { + nearestPatient = pPlayer; + nearestPatientDistance = distance; + } + } + + if (nearestPatient) { + job_struct* newJob = InitialiseNewJob(pBot, JOB_BUFF_ALLY); + if (newJob != nullptr) { + newJob->player = nearestPatient; + newJob->origin = nearestPatient->v.origin; + SubmitNewJob(pBot, JOB_BUFF_ALLY, newJob); } } } @@ -646,7 +710,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { // ignore sniper spots from your team // and ignore sniper spots from your allies - if (sniper_team == pBot->current_team || team_allies[pBot->current_team] & 1 << sniper_team) + if (sniper_team == pBot->current_team || TeamsAreAllied(pBot->current_team, sniper_team)) continue; // ok... check distance to sniper spot and see if its nearish @@ -672,7 +736,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { // don't target your own team's sentry guns... // don't target allied sentry guns either... vecEnd = pent->v.origin + pent->v.view_ofs; // + Vector(0,0,16); - if (pBot->current_team == sentry_team || team_allies[pBot->current_team] & 1 << sentry_team) { + if (pBot->current_team == sentry_team || TeamsAreAllied(pBot->current_team, sentry_team)) { if (VectorsNearerThan(pent->v.origin, pEdict->v.origin, 300.0) && pEdict->v.playerclass != TFC_CLASS_ENGINEER && FInViewCone(vecEnd, pEdict) && FVisible(vecEnd, pEdict)) { // ntf_feature_antigren char *cvar_ntf_feature_antigren = const_cast(CVAR_GET_STRING("ntf_feature_antigren")); @@ -709,7 +773,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { int sentry_team = pent->v.team - 1; // don't target friendly sentry guns... - if (pBot->current_team == sentry_team || team_allies[pBot->current_team] & 1 << sentry_team) + if (pBot->current_team == sentry_team || TeamsAreAllied(pBot->current_team, sentry_team)) continue; // ntf_capture_mg 1 = ignore, we can cap it @@ -777,7 +841,7 @@ static edict_t *BotFindEnemy(bot_t *pBot) { if (mod_id == TFC_DLL) { // don't target your allies either... - if (team_allies[pBot->current_team] & 1 << player_team) + if (TeamsAreAllied(pBot->current_team, player_team)) player_is_ally = true; // so disguised spys wont attack other disguised spys @@ -1346,7 +1410,7 @@ void BotShootAtEnemy(bot_t *pBot) { // don't target your teammates. // and don't target your allies either... - if (pBot->current_team == player_team || team_allies[pBot->current_team] & 1 << player_team) { + if (pBot->current_team == player_team || TeamsAreAllied(pBot->current_team, player_team)) { pBot->strafe_mod = STRAFE_MOD_HEAL; return; } @@ -1801,7 +1865,7 @@ bool BotFireWeapon(const Vector &v_enemy, bot_t *pBot, const int weapon_choice) if (pDelay[select_index].iId != iId) { char msg[80]; snprintf(msg, sizeof(msg), "fire_delay mismatch for weapon id=%d\n", iId); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); return false; } @@ -1819,7 +1883,7 @@ bool BotFireWeapon(const Vector &v_enemy, bot_t *pBot, const int weapon_choice) const int player_team = UTIL_GetTeam(pBot->enemy.ptr); // only heal your teammates or allies... - if ((pBot->current_team == player_team || team_allies[pBot->current_team] & 1 << player_team) && (iId != TF_WEAPON_MEDIKIT && iId != TF_WEAPON_SPANNER)) { + if ((pBot->current_team == player_team || TeamsAreAllied(pBot->current_team, player_team)) && (iId != TF_WEAPON_MEDIKIT && iId != TF_WEAPON_SPANNER)) { pBot->strafe_mod = STRAFE_MOD_HEAL; // return false; // don't "fire" unless weapon is medikit use_primary = false; @@ -2388,7 +2452,7 @@ int PickRandomEnemyTeam(const int my_team) { // count and index the hostile teams for (int index = 0; index < MAX_TEAMS; index++) { - if (is_team[index] == true && my_team != index && !(team_allies[my_team] & 1 << index)) { + if (is_team[index] == true && my_team != index && !TeamsAreAllied(my_team, index)) { teamList[total] = index; ++total; } @@ -2458,7 +2522,7 @@ void BotCheckForMultiguns(bot_t *pBot, float nearestdistance, edict_t *pNewEnemy continue; // don't target friendly sentry guns... - if (pBot->current_team == sentry_team || team_allies[pBot->current_team] & 1 << sentry_team) + if (pBot->current_team == sentry_team || TeamsAreAllied(pBot->current_team, sentry_team)) continue; // is this the closest visible sentry gun? @@ -2493,12 +2557,13 @@ void UpdateFlagCarrierList() { if (pPlayer && !pPlayer->free && pent->v.owner == pPlayer && IsAlive(pPlayer)) { playerHasFlag[i - 1] = true; - const int botIndex = i - 2; // -2 definitely not -1! - - // if the player is a bot set it's flag impulse to match the flag - // so the bot knows which flag it has - if (bots[botIndex + 2].is_used) { - bots[botIndex + 2].flag_impulse = pent->v.impulse; + // bots[] entries are allocated independently of client + // entity slots, so slot - 1 is not a valid bot-record mapping. + // Resolve the record by its entity pointer; humans return -1. + const int botIndex = UTIL_GetBotIndex(pPlayer); + if (botIndex >= 0 && botIndex < MAX_BOTS && + bots[botIndex].is_used && bots[botIndex].pEdict == pPlayer) { + bots[botIndex].flag_impulse = pent->v.impulse; } } } @@ -2534,4 +2599,4 @@ bool PlayerIsInfected(const edict_t *pEntity) { } return false; -} \ No newline at end of file +} diff --git a/bot_compress.h b/bot_compress.h index e9208d1..82ab9c3 100644 --- a/bot_compress.h +++ b/bot_compress.h @@ -49,14 +49,24 @@ inline bool FoxCompressedWrite(std::FILE* fp, const unsigned char* data, const u return false; } - // write header - std::fwrite(FZC_MAGIC, 1, 4, fp); + // Detect header write failures instead of producing a partial + // file that a later map load may try to interpret. + if (std::fwrite(FZC_MAGIC, 1, 4, fp) != 4) { + std::free(compBuf); + return false; + } const unsigned int uncompSize = dataSize; - std::fwrite(&uncompSize, sizeof(unsigned int), 1, fp); + if (std::fwrite(&uncompSize, sizeof(unsigned int), 1, fp) != 1) { + std::free(compBuf); + return false; + } const auto compSizeU = static_cast(compSize); - std::fwrite(&compSizeU, sizeof(unsigned int), 1, fp); + if (std::fwrite(&compSizeU, sizeof(unsigned int), 1, fp) != 1) { + std::free(compBuf); + return false; + } // write compressed data const size_t written = std::fwrite(compBuf, 1, static_cast(compSize), fp); @@ -91,7 +101,12 @@ inline unsigned char* FoxCompressedRead(std::FILE* fp, unsigned int* outSize) if (std::fread(&compSize, sizeof(unsigned int), 1, fp) != 1) return nullptr; - if (uncompSize == 0 || compSize == 0 || uncompSize > 4 * 1024 * 1024) + // Cap both sizes. Previously only the uncompressed size was + // bounded, so a corrupt header could request an enormous compBuf. + constexpr unsigned int MAX_UNCOMPRESSED_SIZE = 4u * 1024u * 1024u; + constexpr unsigned int MAX_COMPRESSED_SIZE = 5u * 1024u * 1024u; + if (uncompSize == 0 || compSize == 0 || + uncompSize > MAX_UNCOMPRESSED_SIZE || compSize > MAX_COMPRESSED_SIZE) return nullptr; // sanity check: 4 MB max auto* compBuf = static_cast(std::malloc(compSize)); diff --git a/bot_ga.cpp b/bot_ga.cpp index 93247c7..e89eab4 100644 --- a/bot_ga.cpp +++ b/bot_ga.cpp @@ -416,11 +416,22 @@ bool FoxGA::load(std::FILE* bfp, const int chromosize) if (!bfp) return false; - if (std::fread(&m_iMaxPopSize, sizeof(unsigned), 1, bfp) != 1) + unsigned loadedMaxPopSize = 0; + unsigned loadedGeneration = 0; + if (std::fread(&loadedMaxPopSize, sizeof(unsigned), 1, bfp) != 1) return false; - if (std::fread(&m_iNumGenerations, sizeof(unsigned), 1, bfp) != 1) + if (std::fread(&loadedGeneration, sizeof(unsigned), 1, bfp) != 1) return false; + // Bound persistent GA state before it controls population + // allocation/iteration. The combat GA normally uses only 16 entries. + if (chromosize <= 0 || chromosize > 1000000 || + loadedMaxPopSize == 0 || loadedMaxPopSize > 1024) + return false; + + m_iMaxPopSize = loadedMaxPopSize; + m_iNumGenerations = loadedGeneration; + return true; } @@ -536,6 +547,12 @@ bool FoxNNGATrained::load(std::FILE* bfp) if (std::fread(&numWeights, sizeof(int), 1, bfp) != 1) return false; + // A saved chromosome must exactly match the initialized NN. + // This also prevents a corrupt count from allocating an arbitrary vector. + if (numWeights < 0 || numWeights > 1000000 || + (numWeights > 0 && numWeights != chromosize)) + return false; + if (numWeights > 0) { std::vector weights(numWeights); diff --git a/bot_job_assessors.cpp b/bot_job_assessors.cpp index 6d4660c..410e00f 100644 --- a/bot_job_assessors.cpp +++ b/bot_job_assessors.cpp @@ -351,8 +351,28 @@ int assess_JobBuffAlly(const bot_t* pBot, const job_struct& r_job) { if (FNullEnt(r_job.player) || !IsAlive(r_job.player) || r_job.f_bufferedTime < pBot->f_killed_time || r_job.f_bufferedTime + 30.0f < pBot->f_think_time) return PRIORITY_NONE; + const bool isMedic = pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC; + const bool isEngineer = pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER; + if (!isMedic && !isEngineer) + return PRIORITY_NONE; + + // The patient must remain on the bot's actual team for the entire job. + if (UTIL_GetTeam(r_job.player) != pBot->current_team) + return PRIORITY_NONE; + + // Medic support survives ordinary distant enemy visibility, + // but close combat or damage received during the last second is an immediate + // threat and still cancels healing. Engineer combat behavior is unchanged. + if ((isMedic && MedicHasImmediateCombatThreat(pBot)) || + (isEngineer && pBot->enemy.ptr != nullptr)) + return PRIORITY_NONE; + + const float patientDistance = (pBot->pEdict->v.origin - r_job.player->v.origin).Length(); + if (patientDistance > SUPPORT_MAX_CHASE_RANGE) + return PRIORITY_NONE; + // a metal wrench doth not cureth the contagion - if (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER) { + if (isEngineer) { if (pBot->m_rgAmmo[weapon_defs[TF_WEAPON_SPANNER].iAmmo1] < 20 // need ammo too || PlayerIsInfected(r_job.player) || PlayerArmorPercent(r_job.player) > 99) return PRIORITY_NONE; @@ -360,14 +380,47 @@ int assess_JobBuffAlly(const bot_t* pBot, const job_struct& r_job) { // check the waypoints validity // and to see if the patient is too far away - if (r_job.phase > 0) { - const int routeDistance = WaypointDistanceFromTo(pBot->current_wp, r_job.waypoint, pBot->current_team); + // Only the waypoint-navigation phase requires a valid route. A nearby, + // visible patient advances directly from phase 0 to phase 2 and deliberately + // has no waypoint; rejecting that job here prevented touch-range healing. + if (r_job.phase == 1) { + if (pBot->current_wp < 0 || !WaypointAvailable(r_job.waypoint, pBot->current_team)) + return PRIORITY_NONE; - if (!WaypointAvailable(r_job.waypoint, pBot->current_team) || routeDistance == -1 || routeDistance > 1500) + const int routeDistance = WaypointDistanceFromTo(pBot->current_wp, r_job.waypoint, pBot->current_team); + if (routeDistance == -1 || routeDistance > SUPPORT_MAX_CHASE_RANGE) return PRIORITY_NONE; } - return jl[JOB_BUFF_ALLY].basePriority; + const int patientIndex = ENTINDEX(r_job.player); + const bool requestedSupport = pBot->f_support_request_time > pBot->f_think_time && + pBot->support_requester_index == patientIndex; + const bool withinUrgentRange = patientDistance <= SUPPORT_DISCOVERY_RANGE; + + if (isMedic) { + const bool needsImmediateCare = r_job.player->v.health < r_job.player->v.max_health || + PlayerIsInfected(r_job.player); + const bool committedToPatient = pBot->currentJob >= 0 && + pBot->currentJob < JOB_BUFFER_MAX && + pBot->jobType[pBot->currentJob] == JOB_BUFF_ALLY && + pBot->job[pBot->currentJob].player == r_job.player && + pBot->f_support_commit_time > pBot->f_think_time; + + // Every nearby injured or infected teammate is urgent, not + // only patients at 50 percent health or below. The short commitment window + // retains urgent priority if either participant briefly crosses the 400-unit + // discovery boundary; the hard 600-unit chase limit still applies. + if ((withinUrgentRange || committedToPatient) && (requestedSupport || needsImmediateCare)) + return MEDIC_HEAL_PRIORITY_URGENT; + + return MEDIC_HEAL_PRIORITY_NORMAL; + } + + const bool criticalArmor = PlayerArmorPercent(r_job.player) <= ENGINEER_CRITICAL_ARMOR_PERCENT; + if (withinUrgentRange && (requestedSupport || criticalArmor)) + return ENGINEER_REPAIR_PRIORITY_URGENT; + + return ENGINEER_REPAIR_PRIORITY_NORMAL; } // assessment function for the priority of a JOB_ESCORT_ALLY job. @@ -964,4 +1017,4 @@ int assess_JobGraffitiArtist(const bot_t* pBot, const job_struct& r_job) { } return jl[JOB_GRAFFITI_ARTIST].basePriority; -} \ No newline at end of file +} diff --git a/bot_job_functions.cpp b/bot_job_functions.cpp index 04b8721..419dbab 100644 --- a/bot_job_functions.cpp +++ b/bot_job_functions.cpp @@ -1490,10 +1490,19 @@ int JobBuffAlly(bot_t* pBot) { return JOB_TERMINATED; } - // phase zero - set a waypoint near where the patient was last seen - // useful when the patient is far away or not visible - if (job_ptr->phase == 0) { - job_ptr->waypoint = WaypointFindNearest_S(job_ptr->origin, nullptr, 500.0f, pBot->current_team, W_FL_DELETED); + // phase zero - set a waypoint near where the patient was last seen + // useful when the patient is far away or not visible + if (job_ptr->phase == 0) { + // A nearby visible patient needs no waypoint. This also + // allows a touching teammate to be healed while the Medic temporarily has + // no current waypoint. + const float allyDistance = (pBot->pEdict->v.origin - job_ptr->player->v.origin).Length(); + if (allyDistance <= SUPPORT_MAX_CHASE_RANGE && FVisible(job_ptr->player->v.origin + job_ptr->player->v.view_ofs, pBot->pEdict)) { + job_ptr->phase = 2; + return JOB_UNDERWAY; + } + + job_ptr->waypoint = WaypointFindNearest_S(job_ptr->origin, nullptr, 500.0f, pBot->current_team, W_FL_DELETED); job_ptr->phase = 1; return JOB_UNDERWAY; @@ -1503,7 +1512,7 @@ int JobBuffAlly(bot_t* pBot) { if (job_ptr->phase == 1) { // go for the ally if they are near and visible const float allyDistance = (pBot->pEdict->v.origin - job_ptr->player->v.origin).Length(); - if (allyDistance < 500.1f && FVisible(job_ptr->player->v.origin + job_ptr->player->v.view_ofs, pBot->pEdict)) { + if (allyDistance <= SUPPORT_MAX_CHASE_RANGE && FVisible(job_ptr->player->v.origin + job_ptr->player->v.view_ofs, pBot->pEdict)) { job_ptr->phase = 2; return JOB_UNDERWAY; } @@ -1524,6 +1533,17 @@ int JobBuffAlly(bot_t* pBot) { // phase 2 - decide how long the bot will try to heal the visible patient if (job_ptr->phase == 2) { + // Once a Medic accepts nearby urgent care, retain that + // patient's urgent priority briefly if movement carries them just beyond + // the 400-unit discovery boundary. Immediate threats are still rejected + // by assess_JobBuffAlly and the 600-unit hard limit remains unchanged. + if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC) { + const float allyDistance = (pBot->pEdict->v.origin - job_ptr->player->v.origin).Length(); + if (allyDistance <= SUPPORT_DISCOVERY_RANGE && + (job_ptr->player->v.health < job_ptr->player->v.max_health || PlayerIsInfected(job_ptr->player))) + pBot->f_support_commit_time = pBot->f_think_time + SUPPORT_COMMIT_DURATION; + } + job_ptr->phase = 3; job_ptr->phase_timer = pBot->f_think_time + random_float(8.0f, 12.0f); } @@ -1536,9 +1556,21 @@ int JobBuffAlly(bot_t* pBot) { return JOB_TERMINATED; } - // go back to looking for the patient if they disappear from view + // Never chase support work beyond the bounded range. const float allyDistance = (pBot->pEdict->v.origin - job_ptr->player->v.origin).Length(); - if (allyDistance >= 500.1f || !FVisible(job_ptr->player->v.origin + job_ptr->player->v.view_ofs, pBot->pEdict)) { + if (allyDistance > SUPPORT_MAX_CHASE_RANGE) + return JOB_TERMINATED; + + // Refresh the short commitment while urgent care remains nearby. This is + // hysteresis, not an uninterruptible lock: immediate combat still cancels + // the job through its assessor on every frame. + if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC && + allyDistance <= SUPPORT_DISCOVERY_RANGE && + (job_ptr->player->v.health < job_ptr->player->v.max_health || PlayerIsInfected(job_ptr->player))) + pBot->f_support_commit_time = pBot->f_think_time + SUPPORT_COMMIT_DURATION; + + // Go back to route planning if the nearby patient disappears from view. + if (!FVisible(job_ptr->player->v.origin + job_ptr->player->v.view_ofs, pBot->pEdict)) { job_ptr->phase = 0; return JOB_UNDERWAY; } @@ -1552,15 +1584,21 @@ int JobBuffAlly(bot_t* pBot) { pBot->strafe_mod = STRAFE_MOD_HEAL; - // make sure the right weapon is selected - if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC && pBot->current_weapon.iId != TF_WEAPON_MEDIKIT) - UTIL_SelectItem(pBot->pEdict, "tf_weapon_medikit"); - else if (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER && pBot->current_weapon.iId != TF_WEAPON_SPANNER) - UTIL_SelectItem(pBot->pEdict, "tf_weapon_spanner"); - - if (allyDistance < 80.0f) - pBot->pEdict->v.button |= IN_ATTACK; - } + // Do not press attack during the weapon-switch frame. The + // old sequence could fire the previously selected gun instead of healing. + if (pBot->pEdict->v.playerclass == TFC_CLASS_MEDIC) { + if (pBot->current_weapon.iId != TF_WEAPON_MEDIKIT) + UTIL_SelectItem(pBot->pEdict, "tf_weapon_medikit"); + else if (allyDistance < 80.0f) + pBot->pEdict->v.button |= IN_ATTACK; + } + else if (pBot->pEdict->v.playerclass == TFC_CLASS_ENGINEER) { + if (pBot->current_weapon.iId != TF_WEAPON_SPANNER) + UTIL_SelectItem(pBot->pEdict, "tf_weapon_spanner"); + else if (allyDistance < 80.0f) + pBot->pEdict->v.button |= IN_ATTACK; + } + } return JOB_UNDERWAY; } diff --git a/bot_job_think.cpp b/bot_job_think.cpp index 0ee2b2e..756f98c 100644 --- a/bot_job_think.cpp +++ b/bot_job_think.cpp @@ -242,7 +242,11 @@ job_struct* InitialiseNewJob(const bot_t* pBot, const int newJobType) { bool SubmitNewJob(bot_t* pBot, const int newJobType, job_struct* newJob) { int i; - if (pBot->current_wp == -1 && newJobType != JOB_SEEK_WAYPOINT) // bit of a kludge but necessary + // Direct support must work even when the bot temporarily has + // no current waypoint. JobBuffAlly can immediately handle a nearby visible + // patient without route data and will terminate safely if navigation is needed + // but no patient waypoint can be found. + if (pBot->current_wp == -1 && newJobType != JOB_SEEK_WAYPOINT && newJobType != JOB_BUFF_ALLY) // bit of a kludge but necessary return false; // many job assessor functions need a valid current waypoint // if the job is currently blacklisted keep it out of the buffer @@ -821,4 +825,4 @@ void BotEngineerThink(bot_t *pBot) { return; } } -} \ No newline at end of file +} diff --git a/bot_neuralnet.cpp b/bot_neuralnet.cpp index 1392123..5227454 100644 --- a/bot_neuralnet.cpp +++ b/bot_neuralnet.cpp @@ -216,11 +216,25 @@ bool FoxNN::load(std::FILE* bfp) if (std::fread(&numOutputs, sizeof(int), 1, bfp) != 1) return false; - init(numInputs, numHidden, numOutputs); + // Do not let a damaged .fnn file request enormous vectors or + // replace the initialized combat network with incompatible dimensions. + if (numInputs <= 0 || numHidden <= 0 || numOutputs <= 0 || + numInputs > 1024 || numHidden > 1024 || numOutputs > 1024) + return false; + if ((m_numInputs > 0 || m_numHidden > 0 || m_numOutputs > 0) && + (numInputs != m_numInputs || numHidden != m_numHidden || numOutputs != m_numOutputs)) + return false; int numWeights; if (std::fread(&numWeights, sizeof(int), 1, bfp) != 1) return false; + const long long expectedWeights = + static_cast(numHidden) * (numInputs + 1LL) + + static_cast(numOutputs) * (numHidden + 1LL); + if (numWeights < 0 || expectedWeights > 1000000LL || numWeights != expectedWeights) + return false; + + init(numInputs, numHidden, numOutputs); std::vector weights(numWeights); if (std::fread(weights.data(), sizeof(float), numWeights, bfp) != static_cast(numWeights)) diff --git a/dll.cpp b/dll.cpp index 0867119..d200747 100644 --- a/dll.cpp +++ b/dll.cpp @@ -103,6 +103,8 @@ int bot_use_grenades = 2; bool bot_team_balance = false; bool bot_bot_balance = false; int bot_bhop = 30; // 0 = off, 1-100 = frequency percentage for bunny hopping +int bot_flag_toss = 1; // 0 = off, 1 = pass a carried flag to nearby human teammates +int bot_flag_toss_distance = 200; // maximum handoff distance in Hammer units int min_bots = -1; int max_bots = -1; int bot_total_varies = 0; @@ -775,9 +777,11 @@ void chatClass::readChatFile() { // This function will return a C ASCII string pointer to a randomly selected // chat message of the type defined by chatSection. -// Some chat strings use a players name with the "%n" specifier, so you can +// Some chat strings use a player's name with the "%s" marker, so you can // specify a players name with playerName, or set it to NULL. void chatClass::pickRandomChatString(char* msg, const size_t maxLength, const int chatSection, const char* playerName) { + if (msg == nullptr || maxLength == 0 || chatSection < 0 || chatSection >= TOTAL_CHAT_TYPES) + return; msg[0] = '\0'; // just in case // make sure this chat section contains at least one chat string @@ -808,13 +812,17 @@ void chatClass::pickRandomChatString(char* msg, const size_t maxLength, const in } this->recent_strings_[chatSection][0] = randomIndex; - // set up the message string - // is "%s" in the text? - if (playerName != nullptr && std::strstr(this->strings_[chatSection][randomIndex].c_str(), "%s") != nullptr) { - snprintf(msg, maxLength, this->strings_[chatSection][randomIndex].c_str(), playerName); + // Chat text is data, not a printf format string. Treat only the supported + // "%s" player-name marker specially so stray '%' characters cannot make + // snprintf consume nonexistent arguments or write through a "%n" token. + const std::string& chatText = this->strings_[chatSection][randomIndex]; + const size_t marker = chatText.find("%s"); + if (marker != std::string::npos) { + const char* replacement = playerName != nullptr ? playerName : "%s"; + snprintf(msg, maxLength, "%.*s%s%s", static_cast(marker), chatText.c_str(), replacement, chatText.c_str() + marker + 2); } else - snprintf(msg, maxLength, this->strings_[chatSection][randomIndex].c_str(), "%s"); + snprintf(msg, maxLength, "%s", chatText.c_str()); msg[maxLength - 1] = '\0'; } @@ -1253,8 +1261,12 @@ void DispatchKeyValue(edict_t* pentKeyvalue, KeyValueData* pkvd) { if (std::strcmp(pkvd->szKeyName, "team_no") == 0) { const int value = std::atoi(pkvd->szValue); - is_team[value - 1] = true; - max_teams = std::max(value, max_teams); + // TFC team arrays contain exactly teams 1 through 4. Malformed or + // unusual map keyvalues must not index before or after those arrays. + if (value >= 1 && value <= MAX_TEAMS) { + is_team[value - 1] = true; + max_teams = std::max(value, max_teams); + } } } } @@ -1295,7 +1307,8 @@ BOOL ClientConnect(edict_t* pEntity, const char* pszName, const char* pszAddress i++; if (i < MAX_BOTS) clients[i] = pEntity; - if (welcome_index == -1) + // Do not retain MAX_BOTS as an array index if every slot is occupied. + if (welcome_index == -1 && i < MAX_BOTS) welcome_index = i; // don't try to add bots for 30 seconds, give client time to get added bot_check_time = gpGlobals->time + 30.0f; @@ -1657,7 +1670,7 @@ void ClientCommand(edict_t* pEntity) { if (arg1 != nullptr) { if (*arg1 != 0) { if (std::strchr(arg1, '\"') == nullptr) - std::strcpy(botname, arg1); + snprintf(botname, sizeof(botname), "%s", arg1); else std::sscanf(arg1, R"("%31s")", &botname[0]); @@ -1716,9 +1729,10 @@ void ClientCommand(edict_t* pEntity) { if (*arg1 != 0) { char message[512]; snprintf(message, sizeof(message), "Waypoint author set to : %s", arg1); - CLIENT_PRINTF(pEntity, print_console, UTIL_VarArgs(message)); - std::strncpy(waypoint_author, arg1, 250); - waypoint_author[251] = '\0'; + // message is already formatted; passing it back through UTIL_VarArgs + // would interpret any '%' in an author name as another format token. + CLIENT_PRINTF(pEntity, print_console, message); + snprintf(waypoint_author, sizeof(waypoint_author), "%s", arg1); hudtextparms_t h; h.channel = 2; @@ -1932,6 +1946,20 @@ void ClientCommand(edict_t* pEntity) { RETURN_META(MRES_SUPERCEDE); return; } + else if (FStrEq(pcmd, "bot_flag_toss")) { + changeBotSetting("bot_flag_toss", &bot_flag_toss, arg1, 0, 1, SETTING_SOURCE_CLIENT_COMMAND); + + if (mr_meta) + RETURN_META(MRES_SUPERCEDE); + return; + } + else if (FStrEq(pcmd, "bot_flag_toss_distance")) { + changeBotSetting("bot_flag_toss_distance", &bot_flag_toss_distance, arg1, 64, 512, SETTING_SOURCE_CLIENT_COMMAND); + + if (mr_meta) + RETURN_META(MRES_SUPERCEDE); + return; + } else if (FStrEq(pcmd, "bot_can_use_teleporter")) // bot_can_use_teleporter - by yuraj { if (FStrEq(arg1, "on")) { @@ -2107,7 +2135,7 @@ void ClientCommand(edict_t* pEntity) { } else if (FStrEq(arg1, "name") && g_area_def) { int i = AreaInsideClosest(pEntity); - if (i != -1 && std::strlen(arg2) < 64) { + if (i != -1 && arg2 != nullptr && std::strlen(arg2) < sizeof(areas[i].namea)) { std::strcpy(areas[i].namea, arg2); std::strcpy(areas[i].nameb, arg2); std::strcpy(areas[i].namec, arg2); @@ -2117,28 +2145,28 @@ void ClientCommand(edict_t* pEntity) { } else if (FStrEq(arg1, "name1") && g_area_def) { int i = AreaInsideClosest(pEntity); - if (i != -1 && std::strlen(arg2) < 64) { + if (i != -1 && arg2 != nullptr && std::strlen(arg2) < sizeof(areas[i].namea)) { std::strcpy(areas[i].namea, arg2); } AreaDefPrintInfo(pEntity); } else if (FStrEq(arg1, "name2") && g_area_def) { int i = AreaInsideClosest(pEntity); - if (i != -1 && std::strlen(arg2) < 64) { + if (i != -1 && arg2 != nullptr && std::strlen(arg2) < sizeof(areas[i].nameb)) { std::strcpy(areas[i].nameb, arg2); } AreaDefPrintInfo(pEntity); } else if (FStrEq(arg1, "name3") && g_area_def) { int i = AreaInsideClosest(pEntity); - if (i != -1 && std::strlen(arg2) < 64) { + if (i != -1 && arg2 != nullptr && std::strlen(arg2) < sizeof(areas[i].namec)) { std::strcpy(areas[i].namec, arg2); } AreaDefPrintInfo(pEntity); } else if (FStrEq(arg1, "name4") && g_area_def) { int i = AreaInsideClosest(pEntity); - if (i != -1 && std::strlen(arg2) < 64) { + if (i != -1 && arg2 != nullptr && std::strlen(arg2) < sizeof(areas[i].named)) { std::strcpy(areas[i].named, arg2); } AreaDefPrintInfo(pEntity); @@ -2520,8 +2548,10 @@ void StartFrame() { // v7 last frame timing pipeCheckFrame = 20; if (gpGlobals->deathmatch) { edict_t* pPlayer; - static float check_server_cmd; - check_server_cmd = gpGlobals->time; + // This is a deadline, not a per-frame timestamp. The old assignment + // reset it on every frame, making the "bot" cvar check (and its clear) + // run at the full server frame rate instead of at the intended interval. + static float check_server_cmd = 0.0f; static int i, index, player_index, bot_index; static float previous_time = -1.0f; static float client_update_time = 0.0f; @@ -2585,6 +2615,8 @@ void StartFrame() { // v7 last frame timing } // start updating client data again client_update_time = gpGlobals->time + 10.0f; bot_check_time = gpGlobals->time + 30.0f; + // Engine time restarts on a map change, so restart this deadline too. + check_server_cmd = gpGlobals->time; } // end of config map check stuff. if (!IS_DEDICATED_SERVER()) { @@ -2627,7 +2659,9 @@ void StartFrame() { // v7 last frame timing } count = 0; UpdateFlagCarrierList(); // need to do this once per frame - for (bot_index = 0; bot_index < gpGlobals->maxClients; bot_index++) { + // bots[] has MAX_BOTS entries even if a game DLL reports a larger limit. + const int bot_slot_limit = std::min(gpGlobals->maxClients, MAX_BOTS); + for (bot_index = 0; bot_index < bot_slot_limit; bot_index++) { // if this bot is active, and the bot is not respawning if (bots[bot_index].is_used && bots[bot_index].respawn_state == RESPAWN_IDLE) { BotThink(&bots[bot_index]); @@ -2665,7 +2699,7 @@ void StartFrame() { // v7 last frame timing char c_class[3]; snprintf(c_skill, sizeof(c_skill), "%d", bots[index1].bot_skill); snprintf(c_team, sizeof(c_team), "%d", bots[index1].bot_team); - snprintf(c_class, sizeof(c_skill), "%d", bots[index1].bot_class); + snprintf(c_class, sizeof(c_class), "%d", bots[index1].bot_class); if (mod_id == TFC_DLL) BotCreate(nullptr, nullptr, nullptr, bots[index1].name, c_skill); else @@ -2703,7 +2737,7 @@ void StartFrame() { // v7 last frame timing if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } if (IS_DEDICATED_SERVER()) bot_cfg_pause_time = gpGlobals->time + 5.0f; @@ -2729,14 +2763,14 @@ void StartFrame() { // v7 last frame timing if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } else { // first say map config not found snprintf(msg, sizeof(msg), "\n%s not found\n", filename); if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); bot_cfg_fp = nullptr; UTIL_BuildFileName(filename, 255, "foxbot.cfg", nullptr); bot_cfg_fp = std::fopen(filename, "r"); @@ -2751,7 +2785,7 @@ void StartFrame() { // v7 last frame timing if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } } // end need config @@ -2777,12 +2811,18 @@ void StartFrame() { // v7 last frame timing } } // if time to check for server commands then do so... if (check_server_cmd <= gpGlobals->time && IS_DEDICATED_SERVER()) { - check_server_cmd = gpGlobals->time + 1.0f; + // Half a second is responsive for console commands while avoiding a + // needless engine-cvar operation on every server frame. + check_server_cmd = gpGlobals->time + 0.5f; char *cvar_bot = const_cast(CVAR_GET_STRING("bot")); if (cvar_bot && cvar_bot[0]) { - char cmd_line[80]; + char cmd_line[256]; char* cmd, * arg1, * arg2, * arg3, * arg4; - std::strcpy(cmd_line, cvar_bot); + // Copy before clearing because CVAR_SET_STRING may invalidate the + // engine-owned pointer. Clearing here also covers handlers that + // return early and prevents the same command from being replayed. + snprintf(cmd_line, sizeof(cmd_line), "%s", cvar_bot); + CVAR_SET_STRING("bot", ""); index = 0; cmd = cmd_line; arg1 = arg2 = arg3 = arg4 = nullptr; // skip to blank or end of string... @@ -2940,6 +2980,12 @@ void StartFrame() { // v7 last frame timing else if (std::strcmp(cmd, "bot_bhop") == 0) { changeBotSetting("bot_bhop", &bot_bhop, arg1, 0, 100, SETTING_SOURCE_SERVER_COMMAND); } + else if (std::strcmp(cmd, "bot_flag_toss") == 0) { + changeBotSetting("bot_flag_toss", &bot_flag_toss, arg1, 0, 1, SETTING_SOURCE_SERVER_COMMAND); + } + else if (std::strcmp(cmd, "bot_flag_toss_distance") == 0) { + changeBotSetting("bot_flag_toss_distance", &bot_flag_toss_distance, arg1, 64, 512, SETTING_SOURCE_SERVER_COMMAND); + } else if (std::strcmp(cmd, "dump") == 0) { edict_t* pent = nullptr; while ((pent = FIND_ENTITY_IN_SPHERE(pent, Vector(0, 0, 0), 8192)) != nullptr && !FNullEnt(pent)) { @@ -2947,7 +2993,6 @@ void StartFrame() { // v7 last frame timing } } // dedicated server input } // moved this line down one - CVAR_SET_STRING("bot", ""); } // check if time to see if a bot needs to be created... if (bot_check_time < gpGlobals->time) { bot_check_time = gpGlobals->time + bot_create_interval; // min/max checking and team balance checking.. @@ -3109,8 +3154,9 @@ void StartFrame() { // v7 last frame timing msg_com_struct* prev = nullptr; msg_com_struct* curr = nullptr; for (i = 0; i < MSG_MAX; i++) { - // assuming i only goes to 64 on next line..see msg_msg[64][msg_max] was [0][i] before...may be a problem - msg_msg[0][i] = '\0'; // clear the messages, for level changes + // Clear each message row. The old [0][i] indexing cleared only the + // first row one character at a time and left 63 stale messages. + msg_msg[i][0] = '\0'; msg_com[i].ifs[0] = '\0'; // the idea behind this delete function is if the root.next isnt null, then it finds the last item in list (the one with item.next =null) and deletes it.. then repeats it all again.. if root.next etc while (msg_com[i].next != nullptr) { @@ -3134,7 +3180,7 @@ void StartFrame() { // v7 last frame timing script_loaded = true; char msg[293]; snprintf(msg, sizeof(msg),"\nExecuting FoXBot TFC script file:%s\n\n", filename); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); int ch = fgetc(bfp); int i1; // Not wanted? [APG]RoboCop[CL] char buffer[14097]; @@ -3196,10 +3242,14 @@ void StartFrame() { // v7 last frame timing if (buffer[i1] == ')') msgsection = 99; // make sure message isnt empty else { // if it isn't empty, move to end (ignore msg) - while (buffer[i1] != ')') { + // Stop at the buffer boundary as well as at ')'. A malformed + // script previously advanced beyond buffer looking for a terminator. + while (i1 < 14096 && buffer[i1] != '\0' && buffer[i1] != ')') { i1++; buf = buf + 1; } + if (buffer[i1] != ')') + random_shit_error = true; } } } // attack @@ -3750,7 +3800,7 @@ void StartFrame() { // v7 last frame timing else if (buffer[i1] != '/' && buffer[i1] != '{' && buffer[i1] != '}' && buffer[i1] != ' ' && buffer[i1] != '\n') { random_shit_error = true; ALERT(at_console, "\\/\\/\\/\\/\\/\\/\n"); - ALERT(at_console, buf); + ALERT(at_console, "%s", buf); ALERT(at_console, "\n"); } // do your magic lexical analysis here. first braces switch (buffer[i1]) { @@ -3861,7 +3911,15 @@ void StartFrame() { // v7 last frame timing } // try and move to end of on start } if (std::strncmp(buf, "on_msg", 6) == 0) { - current_msg++; + // Keep malformed scripts from walking past msg_msg[] and + // msg_com[]. Retaining the final valid slot lets the parser + // continue far enough to report a syntax error safely. + if (current_msg < MSG_MAX - 1) + current_msg++; + else { + current_msg = MSG_MAX - 1; + random_shit_error = true; + } if (msgsection > 0) msgsection = 99; // check for nested msg defs else @@ -3882,14 +3940,18 @@ void StartFrame() { // v7 last frame timing else { cnt = 0; // if it isn't empty, move to end (ignore msg) - while (buffer[i1] != ')') { - msgtext[cnt] = buffer[i1]; - cnt++; + while (i1 < 14096 && buffer[i1] != '\0' && buffer[i1] != ')') { + if (cnt < static_cast(sizeof(msgtext)) - 1) + msgtext[cnt++] = buffer[i1]; + else + random_shit_error = true; i1++; buf = buf + 1; } msgtext[cnt] = '\0'; // terminate string - std::strcpy(msg_msg[current_msg], msgtext); + if (buffer[i1] != ')') + msgsection = 99; // missing closing parenthesis + snprintf(msg_msg[current_msg], sizeof(msg_msg[current_msg]), "%s", msgtext); // now we have the message, we should probably clear out, all the available data for (int i2 = 0; i2 < 8; i2++) { msg_com[current_msg].blue_av[i2] = -1; @@ -4776,7 +4838,7 @@ void StartFrame() { // v7 last frame timing else if (buffer[i1] != '/' && buffer[i1] != '{' && buffer[i1] != '}' && buffer[i1] != ' ' && buffer[i1] != '\n' && random_shit_error == false) { random_shit_error = true; ALERT(at_console, "\\/\\/\\/\\/\\/\\/\n"); - ALERT(at_console, buf); + ALERT(at_console, "%s", buf); ALERT(at_console, "\n"); } // do your magic lexical analysis here.. // first braces switch (buffer[i1]) { @@ -4863,7 +4925,6 @@ C_DLLEXPORT int GetNewDLLFunctions(NEW_DLL_FUNCTIONS* pFunctionTable, int* inter } void FakeClientCommand(edict_t* pBot, const char* arg1, const char* arg2, const char* arg3) { - int length; int i = 0; while (i < 256) { g_argv[i] = '\0'; @@ -4878,19 +4939,21 @@ void FakeClientCommand(edict_t* pBot, const char* arg1, const char* arg2, const } if (arg2 == nullptr || *arg2 == 0) { - length = snprintf(&g_argv[0], 250, "%s", arg1); + snprintf(&g_argv[0], sizeof(g_argv), "%s", arg1); fake_arg_count = 1; } else if (arg3 == nullptr || *arg3 == 0) { - length = snprintf(&g_argv[0], 250, "%s %s", arg1, arg2); + snprintf(&g_argv[0], sizeof(g_argv), "%s %s", arg1, arg2); fake_arg_count = 2; } else { - length = snprintf(&g_argv[0], 250, "%s %s %s", arg1, arg2, arg3); + snprintf(&g_argv[0], sizeof(g_argv), "%s %s %s", arg1, arg2, arg3); fake_arg_count = 3; } isFakeClientCommand = 1; - g_argv[length] = '\0'; // null terminate just in case + // snprintf() returns the length it wanted to write, which may exceed the + // buffer. Indexing with that return value caused an out-of-bounds write. + g_argv[sizeof(g_argv) - 1] = '\0'; if (debug_engine) { global::fp = UTIL_OpenFoxbotLog(); @@ -5256,7 +5319,7 @@ static void ProcessBotCfgFile() { std::fputs(msg, stdout); } else { snprintf(msg, sizeof(msg), "[Config] add bot (%s,%s,%s,%s)\n", arg1 ? arg1 : "null", arg2 ? arg2 : "null", arg3 ? arg3 : "null", arg4 ? arg4 : "null"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } BotCreate(nullptr, arg1, arg2, arg3, arg4); @@ -5331,6 +5394,16 @@ static void ProcessBotCfgFile() { return; } + if (std::strcmp(cmd, "bot_flag_toss") == 0) { + changeBotSetting("bot_flag_toss", &bot_flag_toss, arg1, 0, 1, SETTING_SOURCE_CONFIG_FILE); + return; + } + + if (std::strcmp(cmd, "bot_flag_toss_distance") == 0) { + changeBotSetting("bot_flag_toss_distance", &bot_flag_toss_distance, arg1, 64, 512, SETTING_SOURCE_CONFIG_FILE); + return; + } + if (std::strcmp(cmd, "min_bots") == 0) { changeBotSetting("min_bots", &min_bots, arg1, -1, 31, SETTING_SOURCE_CONFIG_FILE); return; @@ -5415,7 +5488,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot xmas (0) off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else { @@ -5425,7 +5498,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot xmas (1) on\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } return; @@ -5445,7 +5518,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] botdontshoot (0) off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else { @@ -5455,7 +5528,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] botdontshoot (1) on\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } return; @@ -5475,7 +5548,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] botdontmove (0) off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else { @@ -5485,7 +5558,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] botdontmove (1) on\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } return; @@ -5509,7 +5582,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot_can_build_teleporter on\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else if (std::strcmp(arg1, "off") == 0) { //-V547 @@ -5520,7 +5593,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot_can_build_teleporter off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } return; @@ -5539,7 +5612,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot_can_use_teleporter on\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else if (std::strcmp(arg1, "off") == 0) { //-V547 @@ -5550,7 +5623,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot_can_use_teleporter off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } return; @@ -5568,7 +5641,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] pause has been set to %s\n", arg1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } return; } @@ -5587,7 +5660,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] bot_create_interval has been set to %s\n", arg1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } return; } @@ -5608,7 +5681,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] defensive chatter is %s\n", arg1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } return; } @@ -5628,7 +5701,7 @@ static void ProcessBotCfgFile() { } else { snprintf(msg, sizeof(msg), "[Config] offensive chatter is %s\n", arg1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } return; } @@ -5653,7 +5726,7 @@ static void ProcessBotCfgFile() { }*/ snprintf(msg2, sizeof(msg2), "executing: %s\n", server_cmd); - ALERT(at_console, msg2); + ALERT(at_console, "%s", msg2); if (IS_DEDICATED_SERVER()) std::fputs(msg2, stdout); @@ -5849,6 +5922,10 @@ static void DisplayBotInfo() { puts(msg); strncat(msg2, msg, 511 - strlen(msg2)); + snprintf(msg, sizeof(msg), "Bot flag toss %s (distance %d)\n", bot_flag_toss ? "On" : "Off", bot_flag_toss_distance); + puts(msg); + strncat(msg2, msg, 511 - strlen(msg2)); + if (bot_team_balance) snprintf(msg, sizeof(msg), "Bot auto team balance On\n"); else @@ -5899,17 +5976,17 @@ static void DisplayBotInfo() { h.x = 0; h.y = 0; snprintf(msg, sizeof(msg), "--FoxBot Loaded--\n--Visit 'www.apg-clan.org' for updates and info--\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); snprintf(msg2, 511, "%s", msg); /* sprintf(msg,"--* foxbot v%d.%d build# %d *--\n", VER_MAJOR,VER_MINOR,VER_BUILD);*/ snprintf(msg, sizeof(msg), "--* foxbot v%d.%d *--\n", VER_MAJOR, VER_MINOR); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); snprintf(msg, sizeof(msg), "\n--FoxBot info--\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // waypoints @@ -5917,7 +5994,7 @@ static void DisplayBotInfo() { snprintf(msg, sizeof(msg), "Waypoints loaded\n"); else snprintf(msg, sizeof(msg), "Waypoints NOT loaded\n--Warning, bots will not navigate correctly!--\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // area file @@ -5926,7 +6003,7 @@ static void DisplayBotInfo() { else snprintf(msg, sizeof(msg), "Areas not loaded\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // scripts...loaded/passed? @@ -5937,43 +6014,47 @@ static void DisplayBotInfo() { snprintf(msg, sizeof(msg), "Script loaded and NOT parsed\n--Warning script file has an error in it and will NOT be used!--\n"); } else snprintf(msg, sizeof(msg), "No script file loaded\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // now bots vars snprintf(msg, sizeof(msg), "\n--FoxBot vars--\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // bot skill levels snprintf(msg, sizeof(msg), "botskill_lower %d\nbotskill_upper %d\n", botskill_lower, botskill_upper); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); snprintf(msg, sizeof(msg), "max_bots %d\nmin_bots %d\n", max_bots, min_bots); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); // bot chat snprintf(msg, sizeof(msg), "Bot chat %d\n", bot_chat); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); + strncat(msg2, msg, 511 - strlen(msg2)); + + snprintf(msg, sizeof(msg), "Bot flag toss %s (distance %d)\n", bot_flag_toss ? "On" : "Off", bot_flag_toss_distance); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); if (bot_team_balance) snprintf(msg, sizeof(msg), "Bot auto team balance On\n"); else snprintf(msg, sizeof(msg), "Bot auto team balance Off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); if (bot_bot_balance) snprintf(msg, sizeof(msg), "Bot per team balance On\n"); else snprintf(msg, sizeof(msg), "Bot per team balance Off\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); snprintf(msg, sizeof(msg), "\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); strncat(msg2, msg, 511 - strlen(msg2)); ALERT(at_logged, "[FOXBOT]: %s", msg2); ALERT(at_console, "\n\n\n"); @@ -6039,7 +6120,7 @@ static void changeBotSetting(const char* settingName, int* setting, const char* if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } } @@ -6062,7 +6143,7 @@ static void changeBotSetting(const char* settingName, int* setting, const char* if (IS_DEDICATED_SERVER()) std::fputs(msg, stdout); else - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } diff --git a/engine.cpp b/engine.cpp index 3631968..8878ee4 100644 --- a/engine.cpp +++ b/engine.cpp @@ -341,6 +341,8 @@ void pfnClientCommand(edict_t* pEdict, char* szFmt, ...) { }*/ char tempFmt[1024]; + // The legacy engine API declares this format as mutable char*. + static char literalStringFormat[] = "%s"; va_list argp; va_start(argp, szFmt); @@ -369,7 +371,12 @@ void pfnClientCommand(edict_t* pEdict, char* szFmt, ...) { cl_name[0] = '\0'; const char* infobuffer = (*g_engfuncs.pfnGetInfoKeyBuffer)(pEdict); - std::strncpy(cl_name, g_engfuncs.pfnInfoKeyValue(infobuffer, "name"), 120); + // Validate the engine-owned buffer before using it and + // explicitly terminate the copied client name. + if (infobuffer != nullptr) { + std::strncpy(cl_name, g_engfuncs.pfnInfoKeyValue(infobuffer, "name"), sizeof(cl_name) - 1); + cl_name[sizeof(cl_name) - 1] = '\0'; + } //{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,"cl %d name %s\n",i,cl_name); std::fclose(fp); } if (cl_name[0] == '\0' || infobuffer == nullptr) @@ -383,16 +390,16 @@ void pfnClientCommand(edict_t* pEdict, char* szFmt, ...) { // snprintf(sz_error_check,250,"%s b = %d %d\n",sz_error_check,GETPLAYERWONID(pEdict),ENTINDEX(pEdict)); //{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,"b\n"); std::fclose(fp); } // snprintf(sz_error_check,250,"%s -executing",sz_error_check); - (*g_engfuncs.pfnClientCommand)(pEdict, tempFmt); - va_end(argp); + // tempFmt is already formatted data. Forward it through + // a literal format so '%' characters cannot consume absent varargs. + (*g_engfuncs.pfnClientCommand)(pEdict, literalStringFormat, tempFmt); return; } std::strncat(sz_error_check, " !b\n", sizeof(sz_error_check) - std::strlen(sz_error_check) - 1); return; //{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,"!b\n"); std::fclose(fp); } } - (*g_engfuncs.pfnClientCommand)(pEdict, tempFmt); - va_end(argp); + (*g_engfuncs.pfnClientCommand)(pEdict, literalStringFormat, tempFmt); // if(mr_meta) RETURN_META(MRES_HANDLED); //return; } @@ -401,56 +408,27 @@ void pfnClCom(edict_t* pEdict, char* szFmt, ...) { if (debug_engine) { fp = UTIL_OpenFoxbotLog(); if (fp != nullptr) { - std::fprintf(fp, "-pfnClientCom=%s %p\n", szFmt, static_cast(pEdict)); + std::fprintf(fp, "-pfnClientCom=%s %p\n", szFmt ? szFmt : "", static_cast(pEdict)); std::fclose(fp); } } - snprintf(sz_error_check, 250, "-pfnClientCom=%s %p\n", szFmt, static_cast(pEdict)); - if (pEdict != nullptr) { - bool b = false; - - if ((pEdict->v.flags & FL_FAKECLIENT) != FL_FAKECLIENT) { - for (edict_t *&client : clients) { - // if(!((pEdict->v.flags & FL_FAKECLIENT)==FL_FAKECLIENT)) - // bots[i].is_used && - if (client == pEdict) - b = true; - /*if(bots[i].pEdict==pEdict && (GETPLAYERWONID(pEdict)==0 || ENTINDEX(pEdict)==-1 || - (GETPLAYERWONID(pEdict)==-1 && IS_DEDICATED_SERVER()))) - b=false;*/ - } - } - if (b) { - char cl_name[128]; - cl_name[0] = '\0'; - - const char* infobuffer = (*g_engfuncs.pfnGetInfoKeyBuffer)(pEdict); - std::strncpy(cl_name, g_engfuncs.pfnInfoKeyValue(infobuffer, "name"), 120); - //{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,"cl %d name %s\n",i,cl_name); std::fclose(fp); } - if (cl_name[0] == '\0' || infobuffer == nullptr) - b = false; - // unsigned int u=GETPLAYERWONID(pEdict); - // if((u==0 || ENTINDEX(pEdict)==-1)) - // b=false; - } - // if its a bot (b=false) we need to override - if (!b) { - std::strncat(sz_error_check, " !b\n", sizeof(sz_error_check) - std::strlen(sz_error_check) - 1); - // admin mod fix here! ...maybee clientprintf aswell..dunno - // FakeClientCommand(pEdict,szFmt,NULL,NULL); - //{ fp=UTIL_OpenFoxbotLog(); std::fprintf(fp,"!b\n"); std::fclose(fp); } - if (mr_meta) - RETURN_META(MRES_SUPERCEDE); - return; - } - // snprintf(sz_error_check,250,"%s b = %d %d\n",sz_error_check,GETPLAYERWONID(pEdict),ENTINDEX(pEdict)); + snprintf(sz_error_check, 250, "-pfnClientCom=%s %p\n", szFmt ? szFmt : "", static_cast(pEdict)); + + // ClientCommand is an engine call directed to a client, so + // FL_FAKECLIENT is the authoritative distinction. The former clients[] + // and player-name checks could misclassify a connecting human and suppress + // a legitimate command. Null or fake-client targets remain suppressed. + if (pEdict == nullptr || (pEdict->v.flags & FL_FAKECLIENT) == FL_FAKECLIENT) { + std::strncat(sz_error_check, " suppressed\n", sizeof(sz_error_check) - std::strlen(sz_error_check) - 1); + if (mr_meta) + RETURN_META(MRES_SUPERCEDE); return; } + + // Every Metamod hook path must set a result. FoXBot does + // not alter commands for real clients, so allow the original engine call. if (mr_meta) - RETURN_META(MRES_SUPERCEDE); - return; - // if(mr_meta) RETURN_META(MRES_HANDLED); // unreachable code - // return; + RETURN_META(MRES_IGNORED); } void MessageBegin(const int msg_dest, const int msg_type, const float* pOrigin, edict_t* ed) { @@ -1008,48 +986,20 @@ void pfnClPrintf(edict_t* pEdict, PRINT_TYPE ptype, const char* szMsg) { if (debug_engine) { fp = UTIL_OpenFoxbotLog(); if (fp != nullptr) { - std::fprintf(fp, "pfnClPrintf: %p %s\n", static_cast(pEdict), szMsg); + std::fprintf(fp, "pfnClPrintf: %p %s\n", static_cast(pEdict), szMsg ? szMsg : ""); std::fclose(fp); } } - snprintf(sz_error_check, 250, "pfnClPrintf: %p %s\n", static_cast(pEdict), szMsg); + snprintf(sz_error_check, 250, "pfnClPrintf: %p %s\n", static_cast(pEdict), szMsg ? szMsg : ""); - // only send message if its not a bot... - if (pEdict != nullptr) { - bool b = false; - if ((pEdict->v.flags & FL_FAKECLIENT) != FL_FAKECLIENT) { - for (edict_t *&client : clients) { - // if(!((pEdict->v.flags & FL_FAKECLIENT)==FL_FAKECLIENT)) - // bots[i].is_used && - /*if(bots[i].pEdict==pEdict - && (GETPLAYERWONID(pEdict)==0 || ENTINDEX(pEdict)==-1 - || (GETPLAYERWONID(pEdict)==-1 && IS_DEDICATED_SERVER()))) - b=false;*/ - if (client == pEdict) - b = true; - } - } - if (b) { - char cl_name[128]; - cl_name[0] = '\0'; - - const char* infobuffer = (*g_engfuncs.pfnGetInfoKeyBuffer)(pEdict); - std::strncpy(cl_name, g_engfuncs.pfnInfoKeyValue(infobuffer, "name"), 120); - /*{ fp=UTIL_OpenFoxbotLog(); - std::fprintf(fp,"cl %d name %s\n",i,cl_name); std::fclose(fp);}*/ - if (cl_name[0] == '\0' || infobuffer == nullptr) - b = false; - // unsigned int u=GETPLAYERWONID(pEdict); - // if((u==0 || ENTINDEX(pEdict)==-1)) - // b=false; - } - if (b) { - RETURN_META(MRES_HANDLED); - } + // Use the engine's fake-client flag rather than clients[] or + // an InfoKeyBuffer name lookup. Besides being unnecessary, the old code + // dereferenced the info buffer before checking it for null. + if (pEdict == nullptr || (pEdict->v.flags & FL_FAKECLIENT) == FL_FAKECLIENT) RETURN_META(MRES_SUPERCEDE); - } - RETURN_META(MRES_SUPERCEDE); - // RETURN_META(MRES_HANDLED); + + // FoXBot does not modify output for a real client. Let the engine print it. + RETURN_META(MRES_IGNORED); } void pfnServerPrint(const char* szMsg) { @@ -1076,7 +1026,10 @@ void pfnServerPrint(const char* szMsg) { // first compare the message to all bot names, then if bots name is // in message pass to bot // check that the bot that sent a message isn't getting it back - std::strncpy(sz, szMsg, 253); + // All parser buffers must be terminated even when the engine + // supplies an unusually long line. + std::strncpy(sz, szMsg ? szMsg : "", sizeof(sz) - 1); + sz[sizeof(sz) - 1] = '\0'; // clear up sz, and copy start to buffa while (i < 250 && sz[i] != ' ') { msgstart[i] = sz[i]; @@ -1093,7 +1046,8 @@ void pfnServerPrint(const char* szMsg) { // look through the list of active bots for the intended recipient of // the message while (i < MAX_BOTS) { - std::strncpy(buffa, sz, 253); + std::strncpy(buffa, sz, sizeof(buffa) - 1); + buffa[sizeof(buffa) - 1] = '\0'; int k = 1; while (k != 0) { // remove start spaces @@ -1130,8 +1084,10 @@ void pfnServerPrint(const char* szMsg) { if (strcasecmp(cmd, "bots") == 0 && std::strstr(szMsg, "changeclassnow")) continue; - std::strncpy(bots[i].message, szMsg, 253); - std::strncpy(bots[i].msgstart, msgstart, 253); + std::strncpy(bots[i].message, szMsg, sizeof(bots[i].message) - 1); + bots[i].message[sizeof(bots[i].message) - 1] = '\0'; + std::strncpy(bots[i].msgstart, msgstart, sizeof(bots[i].msgstart) - 1); + bots[i].msgstart[sizeof(bots[i].msgstart) - 1] = '\0'; bots[i].newmsg = true; // tell the bot it has mail } } @@ -1146,6 +1102,11 @@ void pfnServerPrint(const char* szMsg) { // This function returns true if the bots name is in the indicated message. static bool name_message_check(const char* msg_string, const char* name_string) { + // The old strlen(name)-1 expression underflowed for an empty + // bot name. Null or empty inputs cannot contain a valid name match. + if (msg_string == nullptr || name_string == nullptr || name_string[0] == '\0') + return false; + const size_t msg_length = std::strlen(msg_string); const size_t name_end = std::strlen(name_string) - static_cast(1); @@ -1215,4 +1176,4 @@ C_DLLEXPORT int GetEngineFunctions(enginefuncs_t* pengfuncsFromEngine, int* inte int GetEngineFunctions_Post(enginefuncs_t* pengfuncsFromEngine, int* interfaceVersion) { pengfuncsFromEngine->pfnRegUserMsg = pfnRegUserMsg_post; return 1; -} \ No newline at end of file +} diff --git a/list.h b/list.h index 280e9df..b8b2f0f 100644 --- a/list.h +++ b/list.h @@ -298,6 +298,9 @@ template void List::remove(LIter& loc) { // store a tmp. ptr to node in the location where // remove will occur. Node* deletedNode = loc.currentNode; + // An iterator already at end() has no node to unlink. + if (deletedNode == nullptr) + return; // attach nodes to the left and right of this node // to each other. Also note that there may not diff --git a/util.cpp b/util.cpp index ce420dc..22c82a3 100644 --- a/util.cpp +++ b/util.cpp @@ -299,13 +299,13 @@ int UTIL_GetTeamColor(edict_t* pEntity) { return -1; if (mod_id == TFC_DLL) { - char topcolor[32]; - const char* infobuffer = (*g_engfuncs.pfnGetInfoKeyBuffer)(pEntity); - const char* value = g_engfuncs.pfnInfoKeyValue(infobuffer, "topcolor"); - if (value == nullptr) - return -1; - snprintf(topcolor, sizeof(topcolor), "%s", value); + // Compare the engine-owned value directly. A client-provided userinfo + // value must not be copied without bounds into a small local buffer. + const char* topcolor = infobuffer != nullptr + ? g_engfuncs.pfnInfoKeyValue(infobuffer, "topcolor") : ""; + if (topcolor == nullptr) + topcolor = ""; // used for spy checking if (std::strcmp(topcolor, "150") == 0 || std::strcmp(topcolor, "153") == 0 || std::strcmp(topcolor, "148") == 0 || std::strcmp(topcolor, "140") == 0) @@ -333,7 +333,9 @@ int UTIL_GetTeam(const edict_t* pEntity) { if (mod_id == TFC_DLL) { // Check if this entity is a player and return the team number // of that player - if (pEntity->v.team - 1 > -1) + // Only TFC playing teams 1-4 are valid internal team + // indices. Spectator is team 6 and must not become index 5. + if (pEntity->v.team >= 1 && pEntity->v.team <= MAX_TEAMS) return pEntity->v.team - 1; // TFC teams are 0-3 based // the team number was invalid, check if this entity is a @@ -415,13 +417,9 @@ int UTIL_GetFlagsTeam(const edict_t* flag_edict) { // return class number 0 through N int UTIL_GetClass(edict_t* pEntity) { - char model_name[32]; - - const char* infobuffer = (*g_engfuncs.pfnGetInfoKeyBuffer)(pEntity); - const char* value = g_engfuncs.pfnInfoKeyValue(infobuffer, "model"); - if (value != nullptr) - snprintf(model_name, sizeof(model_name), "%s", value); - + // Class detection is not implemented here. The previous placeholder copied + // a client-controlled model string into a fixed buffer and then ignored it. + (void)pEntity; return 0; } @@ -656,6 +654,9 @@ void UTIL_BotLogPrintf(const char* fmt, ...) { // attempts to piece together the path and name of the specified file // and/or directory. void UTIL_BuildFileName(char* filename, const int max_fn_length, const char* arg1, const char* arg2) { + if (filename == nullptr || max_fn_length <= 0) + return; + filename[0] = '\0'; ALERT(at_console, "FoXBot: trying to open chat file: %s\n", filename); @@ -673,20 +674,18 @@ void UTIL_BuildFileName(char* filename, const int max_fn_length, const char* arg else return; - // add on the directory and or filename + // Build the complete name with a bounded formatter. The old + // strcat sequence could overrun filename when a path component was long. if (arg1 && *arg1 && arg2 && *arg2) { - std::strcat(filename, arg1); - #ifndef __linux__ - std::strcat(filename, "\\"); + const char separator = '\\'; #else - std::strcat(filename, "/"); + const char separator = '/'; #endif - - std::strcat(filename, arg2); + snprintf(filename, max_fn_length, "%s%s%c%s", foxbot_path, arg1, separator, arg2); } else if (arg1 && *arg1) { - std::strcat(filename, arg1); + snprintf(filename, max_fn_length, "%s%s", foxbot_path, arg1); } filename[max_fn_length - 1] = '\0'; // just to be sure @@ -763,13 +762,18 @@ static void UTIL_FindFoxbotPath() { // It also makes sure that the string is null terminated on success. // It returns false if fgets() returned NULL. bool UTIL_ReadFileLine(char* string, const int max_length, FILE* file_ptr) { + if (string == nullptr || file_ptr == nullptr || max_length <= 1) + return false; + bool line_end_found = false; if (fgets(string, max_length, file_ptr) == nullptr) return false; // check if the string read contains a line terminator of some sort - for (int a = 0; a < max_length; a++) { + // Only inspect characters actually returned by fgets(). + // Scanning to max_length examined uninitialized bytes beyond the '\0'. + for (size_t a = 0; string[a] != '\0'; a++) { if (string[a] == '\n' || string[a] == '\r') line_end_found = true; } @@ -787,4 +791,4 @@ bool UTIL_ReadFileLine(char* string, const int max_length, FILE* file_ptr) { string[max_length - 1] = '\0'; return true; -} \ No newline at end of file +} diff --git a/waypoint.cpp b/waypoint.cpp index ae2c993..39d1a27 100644 --- a/waypoint.cpp +++ b/waypoint.cpp @@ -1099,8 +1099,8 @@ void WaypointAdd(edict_t* pEntity) { //******************************************************** while ((pent = FIND_ENTITY_IN_SPHERE(pent, pEntity->v.origin, radius)) != nullptr && !FNullEnt(pent)) { - char item_name[64]; - std::strcpy(item_name, STRING(pent->v.classname)); + // Avoid copying a map-provided classname into a fixed local buffer. + const char* item_name = STRING(pent->v.classname); if (std::strcmp("item_healthkit", item_name) == 0) { ClientPrint(pEntity, HUD_PRINTCONSOLE, "found a healthkit!\n"); @@ -1511,8 +1511,12 @@ bool WaypointLoad(edict_t* pEntity) { if (IS_DEDICATED_SERVER()) std::printf("loading waypoint file: %s\n", filename); - // read in the waypoint header - std::fread(&header, sizeof header, 1, bfp); + // Reject truncated headers and impossible fixed-array counts + // before consuming any waypoint data. + if (std::fread(&header, sizeof header, 1, bfp) != 1) { + std::fclose(bfp); + return false; + } header.filetype[7] = 0; // null terminate the filetype string @@ -1544,7 +1548,6 @@ bool WaypointLoad(edict_t* pEntity) { std::fclose(bfp); return false; } - // reject corrupt headers before trusting the count [APG]RoboCop[CL] if (header.number_of_waypoints < 0 || header.number_of_waypoints > MAX_WAYPOINTS) { if (pEntity) @@ -1582,7 +1585,11 @@ bool WaypointLoad(edict_t* pEntity) { // read the waypoint data from the file for (i = 0; i < header.number_of_waypoints; i++) { - std::fread(&waypoints[i], sizeof(WAYPOINT), 1, bfp); + if (std::fread(&waypoints[i], sizeof(WAYPOINT), 1, bfp) != 1) { + std::fclose(bfp); + WaypointInit(); + return false; + } ++num_waypoints; // keep track of which waypoint types have been loaded for each team @@ -1609,10 +1616,18 @@ bool WaypointLoad(edict_t* pEntity) { // read and add waypoint paths... for (index = 0; index < num_waypoints; index++) { // read the number of paths from this node... - std::fread(&num, sizeof num, 1, bfp); + if (std::fread(&num, sizeof num, 1, bfp) != 1 || num < 0 || num > MAX_WAYPOINTS) { + std::fclose(bfp); + WaypointInit(); + return false; + } for (i = 0; i < num; i++) { - std::fread(&path_index, sizeof path_index, 1, bfp); + if (std::fread(&path_index, sizeof path_index, 1, bfp) != 1) { + std::fclose(bfp); + WaypointInit(); + return false; + } WaypointAddPath(index, path_index); } @@ -1650,7 +1665,10 @@ bool WaypointLoad(edict_t* pEntity) { if (bfp != nullptr) { if (IS_DEDICATED_SERVER()) std::printf("loading waypoint file: %s\n", filename); - std::fread(&header, sizeof header, 1, bfp); + if (std::fread(&header, sizeof header, 1, bfp) != 1) { + std::fclose(bfp); + return false; + } header.filetype[7] = 0; if (std::strcmp(header.filetype, "HPB_bot") == 0) { @@ -1661,7 +1679,6 @@ bool WaypointLoad(edict_t* pEntity) { std::fclose(bfp); return false; } - header.mapname[31] = 0; if (std::strcmp(header.mapname, STRING(gpGlobals->mapname)) == 0) { @@ -1676,17 +1693,29 @@ bool WaypointLoad(edict_t* pEntity) { WaypointInit(); // remove any existing waypoints for (i = 0; i < header.number_of_waypoints; i++) { - std::fread(&waypoints[i], sizeof waypoints[0], 1, bfp); + if (std::fread(&waypoints[i], sizeof waypoints[0], 1, bfp) != 1) { + std::fclose(bfp); + WaypointInit(); + return false; + } num_waypoints++; } // read and add waypoint paths... for (index = 0; index < num_waypoints; index++) { // read the number of paths from this node... - std::fread(&num, sizeof num, 1, bfp); + if (std::fread(&num, sizeof num, 1, bfp) != 1 || num < 0 || num > MAX_WAYPOINTS) { + std::fclose(bfp); + WaypointInit(); + return false; + } for (i = 0; i < num; i++) { - std::fread(&path_index, sizeof path_index, 1, bfp); + if (std::fread(&path_index, sizeof path_index, 1, bfp) != 1) { + std::fclose(bfp); + WaypointInit(); + return false; + } WaypointAddPath(index, path_index); } @@ -1754,11 +1783,18 @@ static bool WaypointLoadVersion4(FILE* bfp, const int number_of_waypoints) { short num; short path_index; + // A file-provided count must fit the fixed waypoint array. + if (bfp == nullptr || number_of_waypoints < 0 || number_of_waypoints > MAX_WAYPOINTS) + return false; + WaypointInit(); // remove any existing waypoints // read the waypoint data from the file for (i = 0; i < number_of_waypoints; i++) { - std::fread(&dummy_waypoint, sizeof(WAYPOINT_VERSION4), 1, bfp); + if (std::fread(&dummy_waypoint, sizeof(WAYPOINT_VERSION4), 1, bfp) != 1) { + WaypointInit(); + return false; + } // convert version 4 data to version 5 data @@ -1814,10 +1850,16 @@ static bool WaypointLoadVersion4(FILE* bfp, const int number_of_waypoints) { // read and add waypoint paths... for (int index = 0; index < num_waypoints; index++) { // read the number of paths from this node... - std::fread(&num, sizeof num, 1, bfp); + if (std::fread(&num, sizeof num, 1, bfp) != 1 || num < 0 || num > MAX_WAYPOINTS) { + WaypointInit(); + return false; + } for (i = 0; i < num; i++) { - std::fread(&path_index, sizeof path_index, 1, bfp); + if (std::fread(&path_index, sizeof path_index, 1, bfp) != 1) { + WaypointInit(); + return false; + } WaypointAddPath(index, path_index); } @@ -3100,17 +3142,25 @@ static void WaypointRouteInit() { if (build_matrix[matrix]) { if (shortest_path[matrix] == nullptr) { snprintf(msg, sizeof(msg), "calculating FoXBot waypoint paths for team %d...\n", matrix + 1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); shortest_path[matrix] = static_cast(std::malloc(sizeof(unsigned int) * array_size)); - if (shortest_path[matrix] == nullptr) + // Reporting an allocation failure is not sufficient; + // the old code immediately dereferenced the null pointer. + if (shortest_path[matrix] == nullptr) { ALERT(at_error, "FoXBot - Error allocating memory for shortest path!"); + return; + } from_to[matrix] = static_cast(std::malloc(sizeof(unsigned int) * array_size)); - if (from_to[matrix] == nullptr) + if (from_to[matrix] == nullptr) { ALERT(at_error, "FoXBot - Error allocating memory for from to matrix!"); + std::free(shortest_path[matrix]); + shortest_path[matrix] = nullptr; + return; + } unsigned int* pShortestPath = shortest_path[matrix]; unsigned int* pFromTo = from_to[matrix]; @@ -3131,6 +3181,12 @@ static void WaypointRouteInit() { while (i < MAX_PATH_INDEX) { if (p->index[i] != -1) { index = p->index[i]; + // Tolerate old or damaged path lists without + // indexing beyond the loaded waypoint set. + if (index >= route_num_waypoints) { + i++; + continue; + } // check if this is NOT team specific OR // matches this team @@ -3141,7 +3197,7 @@ static void WaypointRouteInit() { if (distance > REACHABLE_RANGE) { snprintf(msg, sizeof(msg), "Waypoint path distance > %4.1f at from %d to %d\n", REACHABLE_RANGE, static_cast(row), static_cast(index)); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); WaypointDeletePath(row, index); } else { @@ -3169,7 +3225,7 @@ static void WaypointRouteInit() { pFromTo[a * route_num_waypoints + b] = WAYPOINT_UNREACHABLE; } snprintf(msg, sizeof(msg), "FoXBot waypoint path calculations for team %d complete!\n", matrix + 1); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } } @@ -3214,7 +3270,7 @@ static void WaypointRouteInit() { } } snprintf(msg, sizeof(msg), "RJ/Conc Total: %d : Blue: %d : Red: %d : Yellow: %d : Green: %d\n", RJIndex + 1, teamCount[0], teamCount[1], teamCount[2], teamCount[3]); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } // return the next waypoint index for a path from the Floyd matrix when @@ -3932,6 +3988,12 @@ void AreaDefSave() { UTIL_BuildFileName(filename, 255, "areas", mapname); std::FILE* bfp = std::fopen(filename, "wb"); + // Do not pass a null FILE pointer to fwrite/fclose when the + // service account cannot create or replace the area file. + if (bfp == nullptr) { + ALERT(at_error, "FoXBot - Unable to create area file!\n"); + return; + } // write the waypoint header to the file... std::fwrite(&header, sizeof header, 1, bfp); @@ -3962,7 +4024,10 @@ bool AreaDefLoad(edict_t* pEntity) { char msg[256]; if (IS_DEDICATED_SERVER()) std::printf("loading area file: %s\n", filename); - std::fread(&header, sizeof header, 1, bfp); + if (std::fread(&header, sizeof header, 1, bfp) != 1) { + std::fclose(bfp); + return false; + } header.filetype[7] = 0; if (std::strcmp(header.filetype, "FoXBot") == 0) { @@ -3974,7 +4039,13 @@ bool AreaDefLoad(edict_t* pEntity) { return false; } - header.mapname[31] = 0; + header.mapname[31] = 0; + + if (header.number_of_areas < 0 || header.number_of_areas > MAX_WAYPOINTS) { + ALERT(at_error, "FoXBot - Invalid area count in area file!\n"); + std::fclose(bfp); + return false; + } if (strcasecmp(header.mapname, STRING(gpGlobals->mapname)) == 0) { // works for areas aswell :) @@ -4000,7 +4071,11 @@ bool AreaDefLoad(edict_t* pEntity) { ClientPrint(pEntity, HUD_PRINTNOTIFY, "Loading FoXBot area file\n"); for (i = 0; i < header.number_of_areas; i++) { - std::fread(&areas[i], sizeof areas[0], 1, bfp); + if (std::fread(&areas[i], sizeof areas[0], 1, bfp) != 1) { + std::fclose(bfp); + num_areas = 0; + return false; + } num_areas++; } } @@ -4174,7 +4249,8 @@ void AreaAutoBuild1() { int lc, rc, r, l; bool ru, lu, rd, ld; int lr, ll; - for (i = 0; i <= num_waypoints; i++) { + // num_waypoints/num_areas are counts, not final indices. + for (i = 0; i < num_waypoints; i++) { if (!(waypoints[i].flags & W_FL_DELETED)) { if (num_areas >= MAX_WAYPOINTS) return; @@ -4220,7 +4296,7 @@ void AreaAutoBuild1() { ll = 0; lr = 0; double epsilon = 0.0001; // Define your own level of precision - while (k <= num_waypoints) { + while (k < num_waypoints) { if (std::abs(waypoints[i].origin.y - waypoints[k].origin.y) < epsilon && std::abs(waypoints[i].origin.z - waypoints[k].origin.z) < epsilon && i != k) { if (std::abs(waypoints[i].origin.x - 32.0 * (lc + 1) - waypoints[k].origin.x) < epsilon) { k = -1; @@ -4284,7 +4360,7 @@ void AreaAutoBuild1() { } while (expanded) { expanded = false; - for (j = 0; j <= num_waypoints; j++) { + for (j = 0; j < num_waypoints; j++) { if (!(waypoints[j].flags & W_FL_DELETED)) { // expand via y // and no slopeing in z @@ -4301,7 +4377,7 @@ void AreaAutoBuild1() { ld = false; ll = 0; lr = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (std::abs(waypoints[j].origin.y - waypoints[k].origin.y) < epsilon && std::abs(waypoints[j].origin.z - waypoints[k].origin.z) < epsilon && j != k) { if (std::abs(waypoints[j].origin.x - 32.0 * (l + 1) - waypoints[k].origin.x) < epsilon) { @@ -4390,7 +4466,7 @@ void AreaAutoBuild1() { ld = false; ll = 0; lr = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (std::abs(waypoints[j].origin.y - waypoints[k].origin.y) < epsilon && std::abs(waypoints[j].origin.z - waypoints[k].origin.z) < epsilon && j != k) { if (std::abs(waypoints[j].origin.x - 32.0 * (l + 1) - waypoints[k].origin.x) < epsilon) { @@ -4476,7 +4552,7 @@ void AreaAutoBuild1() { } // now all areas have been created (from all the wpts) // their will be lots of parallel areas that can be merged... - for (i = 0; i <= num_areas; i++) { + for (i = 0; i < num_areas; i++) { if (!(areas[i].flags & W_FL_DELETED)) { if ((areas[i].flags & A_FL_1) == A_FL_1 && (areas[i].flags & A_FL_2) == A_FL_2 && (areas[i].flags & A_FL_3) == A_FL_3 && (areas[i].flags & A_FL_4) == A_FL_4) { lc = 0; @@ -4492,7 +4568,7 @@ void AreaAutoBuild1() { ld = false; ll = 0; lr = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (waypoints[k].origin == areas[i].a + Vector(16, 16, 0)) { h = k; k = num_waypoints; @@ -4501,7 +4577,7 @@ void AreaAutoBuild1() { } k = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (std::abs(waypoints[h].origin.x - waypoints[k].origin.x) < epsilon && std::abs(waypoints[h].origin.z - waypoints[k].origin.z) < epsilon && h != k) { if (std::abs(waypoints[h].origin.y - 32.0 * (lc + 1) - waypoints[k].origin.y) < epsilon) { @@ -4578,7 +4654,7 @@ void AreaAutoBuild1() { bool expanded = true; while (expanded) { expanded = false; - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED)) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (i != j) { @@ -4587,7 +4663,7 @@ void AreaAutoBuild1() { l = 0; k = 0; h = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (waypoints[k].origin == areas[j].d + Vector(-16, 16, 0)) { h = k; k = num_waypoints; @@ -4595,7 +4671,7 @@ void AreaAutoBuild1() { k++; } k = 0; - while (k <= num_waypoints) { + while (k < num_waypoints) { if (std::abs(waypoints[h].origin.x - waypoints[k].origin.x) < epsilon && std::abs(waypoints[h].origin.z - waypoints[k].origin.z) < epsilon && h != k) { if (std::abs(waypoints[h].origin.y - 32.0 * (l + 1) - waypoints[k].origin.y) < epsilon) { k = -1; @@ -4691,14 +4767,14 @@ void AreaAutoMerge() { int stk[stk_sz]; int stk_cnt; - for (i = 0; i <= num_areas; i++) { + for (i = 0; i < num_areas; i++) { if (!(areas[i].flags & W_FL_DELETED)) { if ((areas[i].flags & A_FL_1) == A_FL_1 && (areas[i].flags & A_FL_2) == A_FL_2 && (areas[i].flags & A_FL_3) == A_FL_3 && (areas[i].flags & A_FL_4) == A_FL_4) { a = false; b = false; c = false; d = false; - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (areas[j].a == areas[i].a || areas[j].b == areas[i].a || areas[j].c == areas[i].a || areas[j].d == areas[i].a) @@ -4723,7 +4799,7 @@ void AreaAutoMerge() { merged = false; if (areas[i].d.x - areas[i].a.x > areas[i].b.y - areas[i].a.y) { // x>y so expand in the y direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !d) { @@ -4806,7 +4882,7 @@ void AreaAutoMerge() { } else { // x<=y so expand in the x direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !b) { @@ -4895,14 +4971,14 @@ void AreaAutoMerge() { } // clear the remaining shit up!! - for (i = 0; i <= num_areas; i++) { + for (i = 0; i < num_areas; i++) { if (!(areas[i].flags & W_FL_DELETED)) { if ((areas[i].flags & A_FL_1) == A_FL_1 && (areas[i].flags & A_FL_2) == A_FL_2 && (areas[i].flags & A_FL_3) == A_FL_3 && (areas[i].flags & A_FL_4) == A_FL_4) { a = false; b = false; c = false; d = false; - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (areas[j].a == areas[i].a || areas[j].b == areas[i].a || areas[j].c == areas[i].a || areas[j].d == areas[i].a) @@ -4927,7 +5003,7 @@ void AreaAutoMerge() { merged = false; if (areas[i].d.x - areas[i].a.x > areas[i].b.y - areas[i].a.y) { // x>y so expand in the y direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !d) { @@ -5010,7 +5086,7 @@ void AreaAutoMerge() { } else { // x<=y so expand in the x direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !b) { @@ -5098,7 +5174,7 @@ void AreaAutoMerge() { } } // and the final lot? - for (i = 0; i <= num_areas; i++) { + for (i = 0; i < num_areas; i++) { if (!(areas[i].flags & W_FL_DELETED)) { if ((areas[i].flags & A_FL_1) == A_FL_1 && (areas[i].flags & A_FL_2) == A_FL_2 && (areas[i].flags & A_FL_3) == A_FL_3 && (areas[i].flags & A_FL_4) == A_FL_4) { a = false; @@ -5114,7 +5190,7 @@ void AreaAutoMerge() { stk_cnt = 0; merged = false; // x>y so expand in the y direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !d) { @@ -5195,7 +5271,7 @@ void AreaAutoMerge() { } } // x<=y so expand in the x direction - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { if (!a || !b) { @@ -5288,13 +5364,13 @@ void AreaAutoMerge() { // a)size of adjacent area matches // b)step is the same (more than 2 areas) // c)same direction as first ones found - for (i = 0; i <= num_areas; i++) { + for (i = 0; i < num_areas; i++) { if (!(areas[i].flags & W_FL_DELETED)) { if ((areas[i].flags & A_FL_1) == A_FL_1 && (areas[i].flags & A_FL_2) == A_FL_2 && (areas[i].flags & A_FL_3) == A_FL_3 && (areas[i].flags & A_FL_4) == A_FL_4) { float sx, sy; sx = areas[i].d.x - areas[i].a.x; sy = areas[i].b.y - areas[i].a.y; - for (j = 0; j <= num_areas; j++) { + for (j = 0; j < num_areas; j++) { if (!(areas[j].flags & W_FL_DELETED) && i != j) { if ((areas[j].flags & A_FL_1) == A_FL_1 && (areas[j].flags & A_FL_2) == A_FL_2 && (areas[j].flags & A_FL_3) == A_FL_3 && (areas[j].flags & A_FL_4) == A_FL_4) { // find neighbours to i @@ -5318,7 +5394,7 @@ void AreaAutoMerge() { // j=-1; // now search for neighbours with the same z // and same size - for (k = 0; k <= num_areas; k++) { + for (k = 0; k < num_areas; k++) { if (!(areas[k].flags & W_FL_DELETED) && j != k && i != k) { if ((areas[k].flags & A_FL_1) == A_FL_1 && (areas[k].flags & A_FL_2) == A_FL_2 && (areas[k].flags & A_FL_3) == A_FL_3 && (areas[k].flags & A_FL_4) == A_FL_4) { float zz; @@ -5386,7 +5462,7 @@ void AreaAutoMerge() { // j=-1; // now search for neighbours with the same z // and same size - for (k = 0; k <= num_areas; k++) { + for (k = 0; k < num_areas; k++) { if (!(areas[k].flags & W_FL_DELETED) && j != k && i != k) { if ((areas[k].flags & A_FL_1) == A_FL_1 && (areas[k].flags & A_FL_2) == A_FL_2 && (areas[k].flags & A_FL_3) == A_FL_3 && (areas[k].flags & A_FL_4) == A_FL_4) { float zz; @@ -5453,11 +5529,14 @@ void ProcessCommanderList() { char msg[255]; char buffer[80]; char filename[255]; - //// delete dynamic memory - // LIter iter(&commanders); - // for(iter.begin(); !iter.end(); ++iter) - //{ - //} + // List owns only its nodes, not the allocated strings. + // Release those strings before rebuilding the list on each map load. + LIter oldCommander(&commanders); + for (oldCommander.begin(); !oldCommander.end(); ++oldCommander) { + char** value = oldCommander.current(); + if (value != nullptr) + delete[] *value; + } commanders.clear(); constexpr char invalidChars[] = " abcdefghijklmnopqrstuvwxyz,./<>?;'\"[]{}-=+!@#$%^&*()"; @@ -5469,7 +5548,7 @@ void ProcessCommanderList() { std::printf("[Config] Reading foxbot_commanders.txt\n"); else { snprintf(msg, sizeof(msg), "[Config] Reading foxbot_commanders.txt\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } else { @@ -5477,7 +5556,7 @@ void ProcessCommanderList() { std::printf("[Config] Couldn't open foxbot_commanders.txt\n"); else { snprintf(msg, sizeof(msg), "[Config] Couldn't open foxbot_commanders.txt\n"); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } return; } @@ -5514,7 +5593,7 @@ void ProcessCommanderList() { std::printf("[Config] foxbot_commanders.txt : Invalid Character %c\n", ch); else { snprintf(msg, sizeof(msg), "[Config] foxbot_commanders.txt : Invalid Character %c\n", ch); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } } @@ -5525,10 +5604,11 @@ void ProcessCommanderList() { char* uId = new char[80]; std::strcpy(uId, buffer); - // Get rid of line feeds - if (const size_t len = std::strlen(uId); len > 0 && (uId[len - 1] == '\n' || uId[len - 1] == '\r' || uId[len - 1] == '\0')) { - uId[len - 1] = '\0'; - } + // Remove both bytes of Windows CRLF, not just the final + // byte, so the stored ID compares exactly with the live ID. + size_t len = std::strlen(uId); + while (len > 0 && (uId[len - 1] == '\n' || uId[len - 1] == '\r')) + uId[--len] = '\0'; fp = UTIL_OpenFoxbotLog(); if (fp != nullptr) { @@ -5541,7 +5621,7 @@ void ProcessCommanderList() { std::printf("[Config] foxbot_commanders.txt : Loaded User %s\n", buffer); else { snprintf(msg, sizeof(msg), "[Config] foxbot_commanders.txt : Loaded User %s\n", buffer); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } } } @@ -5550,7 +5630,7 @@ void ProcessCommanderList() { std::printf("[Config] foxbot_commanders.txt : Loaded %d users\n", commanders.size()); else { snprintf(msg, sizeof(msg), "[Config] foxbot_commanders.txt : Loaded %d users\n", commanders.size()); - ALERT(at_console, msg); + ALERT(at_console, "%s", msg); } std::fclose(inFile); -} \ No newline at end of file +} From 9964cab009665c2322894e8047125a5387af6630 Mon Sep 17 00:00:00 2001 From: Angry Bishop <183974626+Angry-Bishop@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:47:53 -0700 Subject: [PATCH 2/2] Prevent bots from holding primed grenades after combat --- bot.cpp | 6 +++ bot_combat.cpp | 91 +++++++++++++++++++++++++------------------ bot_func.h | 4 ++ bot_job_functions.cpp | 18 +++++---- bot_job_think.cpp | 10 ++--- 5 files changed, 79 insertions(+), 50 deletions(-) diff --git a/bot.cpp b/bot.cpp index 8136ed1..e725398 100644 --- a/bot.cpp +++ b/bot.cpp @@ -3897,6 +3897,9 @@ void BotThink(bot_t* pBot) { // human snipers can use their rifles as golf clubs [APG]RoboCop[CL] if (botdontshoot) pBot->enemy.ptr = nullptr; + // Blinded or deliberately stationary bots bypass normal combat processing, + // but must still release an already-live grenade by its safety deadline. + BotCheckPrimedGrenade(pBot); if (botdontmove) { pBot->f_move_speed = 0.0f; pBot->f_side_speed = 0.0f; @@ -3942,6 +3945,9 @@ void BotThink(bot_t* pBot) { // these four functions essentially handle the core of bot behaviour BotSenseEnvironment(pBot); + // Check again after enemy selection, which can clear the target immediately + // after a kill while the grenade still needs to be aimed away. + BotCheckPrimedGrenade(pBot); BotFight(pBot); BotJobThink(pBot); BotRunJobs(pBot); diff --git a/bot_combat.cpp b/bot_combat.cpp index c0d0335..8565b4a 100644 --- a/bot_combat.cpp +++ b/bot_combat.cpp @@ -1956,6 +1956,54 @@ bool BotFireWeapon(const Vector &v_enemy, bot_t *pBot, const int weapon_choice) // checking whether or not the sentry they have in memory is viewable // from there. If so they will anticipate contact and prime a grenade, // and acquire the target early just before contact. +static float BotGrenadeReleaseTime(const bot_t* pBot) { + const int lostHealthPercent = 100 - PlayerHealthPercent(pBot->pEdict); + float releaseTime = (pBot->nadeType == GRENADE_MIRV) ? 1.0f : 0.8f; + + // Injured bots release earlier because they can tolerate less splash damage. + if (lostHealthPercent > 20) + releaseTime += static_cast(lostHealthPercent) * 0.01f; + + return releaseTime; +} + +// Release both grenade buttons and keep the bot's grenade state synchronized. +// Sending both releases is the established TFC-safe way to handle either slot. +void BotReleasePrimedGrenade(bot_t* pBot) { + if (pBot == nullptr || pBot->pEdict == nullptr || !pBot->nadePrimed) + return; + + FakeClientCommand(pBot->pEdict, "-gren1", "102", nullptr); + FakeClientCommand(pBot->pEdict, "-gren2", "101", nullptr); + pBot->nadePrimed = false; + pBot->nadeType = 0; +} + +// Maintain an already-primed grenade even when normal combat processing stops. +// The disposal job provides ordinary aiming when time permits; the deadline +// merely releases the grenade in the bot's current direction before detonation. +void BotCheckPrimedGrenade(bot_t* pBot) { + if (pBot == nullptr || pBot->pEdict == nullptr || !pBot->nadePrimed) + return; + + const float timeToDet = 4.0f - (pBot->f_think_time - pBot->primeTime); + + // This is the same release deadline used during normal combat. It must not + // depend on bot_use_grenades: that setting may change after a grenade is live. + if (timeToDet <= BotGrenadeReleaseTime(pBot)) { + BotReleasePrimedGrenade(pBot); + return; + } + + // If the target disappeared, start looking for a safe throw direction now + // rather than waiting until the last two seconds of the fuse. + if (pBot->enemy.ptr == nullptr) { + job_struct* newJob = InitialiseNewJob(pBot, JOB_BIN_GRENADE); + if (newJob != nullptr) + SubmitNewJob(pBot, JOB_BIN_GRENADE, newJob); + } +} + int BotNadeHandler(bot_t *pBot, bool timed, const char newNadeType) { // Lets try putting discard code in here. (dont let the engineer discard) if (pBot->f_discard_time < pBot->f_think_time && pBot->pEdict->v.playerclass != TFC_CLASS_ENGINEER) { @@ -1985,14 +2033,6 @@ int BotNadeHandler(bot_t *pBot, bool timed, const char newNadeType) { const float timeToDet = 4.0f - (pBot->f_think_time - pBot->primeTime); float zDiff = 0; - // if the bot has no target to throw at try to find a place to - // dispose of the live grenade(anti-suicide code) - if (pBot->nadePrimed == true && pBot->enemy.ptr == nullptr && timeToDet <= 2.0f) { - job_struct *newJob = InitialiseNewJob(pBot, JOB_BIN_GRENADE); - if (newJob != nullptr) - SubmitNewJob(pBot, JOB_BIN_GRENADE, newJob); - } - // emergency throw if the enemy has closed to self-damage range while // the bot is holding a live grenade - prevents suicide bomber behaviour - [APG]RoboCop[CL] if (pBot->nadePrimed && pBot->enemy.ptr != nullptr) { @@ -2010,25 +2050,8 @@ int BotNadeHandler(bot_t *pBot, bool timed, const char newNadeType) { } // Go ahead and throw if its about to explode. - if (pBot->nadePrimed) { - const int lost_health_percent = 100 - PlayerHealthPercent(pEdict); - float release_time = 0.8f; - - if (pBot->nadeType == GRENADE_MIRV) - release_time = 1.0f; - - // factor in the bots state of health - if (lost_health_percent > 20) - release_time += static_cast(lost_health_percent) * 0.01f; - - if (timeToDet <= release_time) { - // char msg[96]; - // std::sprintf(msg, "Tossing. release_time:%f lost_health_percent:%d", - // release_time, lost_health_percent); - // UTIL_HostSay(pBot->pEdict, 0, msg);//DebugMessageOfDoom! - toss = true; - } - } + if (pBot->nadePrimed && timeToDet <= BotGrenadeReleaseTime(pBot)) + toss = true; // Elevation check, try to throw up to ledges better. if (pBot->enemy.ptr) { @@ -2071,13 +2094,9 @@ int BotNadeHandler(bot_t *pBot, bool timed, const char newNadeType) { } // Time to throw? - if (toss || pEdict->v.waterlevel == WL_HEAD_IN_WATER) { - // Throw the mofos! - FakeClientCommand(pEdict, "-gren1", "102", nullptr); - FakeClientCommand(pEdict, "-gren2", "101", nullptr); + if ((toss || pEdict->v.waterlevel == WL_HEAD_IN_WATER) && pBot->nadePrimed) { + BotReleasePrimedGrenade(pBot); rtnValue = 1; - pBot->nadePrimed = false; - pBot->nadeType = 0; } /* // this code allows bots to prime grenades early when @@ -2268,12 +2287,10 @@ int BotNadeHandler(bot_t *pBot, bool timed, const char newNadeType) { } // Go ahead and toss em if they aren't meant to be timed. - if (!timed) { - FakeClientCommand(pEdict, "-gren1", "102", nullptr); - FakeClientCommand(pEdict, "-gren2", "101", nullptr); + if (!timed && pBot->nadePrimed) { pBot->tossNade = 1; + BotReleasePrimedGrenade(pBot); rtnValue = 1; - pBot->nadePrimed = false; } return rtnValue; diff --git a/bot_func.h b/bot_func.h index af598b2..26d58ef 100644 --- a/bot_func.h +++ b/bot_func.h @@ -61,6 +61,10 @@ void BotShootAtEnemy(bot_t* pBot); // DrEvils functions. int BotNadeHandler(bot_t* pBot, bool timed, char newNadeType); +void BotCheckPrimedGrenade(bot_t* pBot); + +void BotReleasePrimedGrenade(bot_t* pBot); + int BotAssessThreatLevel(const bot_t* pBot); int BotTeamColorCheck(const edict_t* pent); diff --git a/bot_job_functions.cpp b/bot_job_functions.cpp index 419dbab..21d1790 100644 --- a/bot_job_functions.cpp +++ b/bot_job_functions.cpp @@ -3648,10 +3648,7 @@ int JobBinGrenade(bot_t* pBot) { // regardless of which phase the bot is in - [APG]RoboCop[CL] const float timeToDet = 4.0f - (pBot->f_think_time - pBot->primeTime); if (pBot->nadePrimed && timeToDet <= 0.5f) { - FakeClientCommand(pBot->pEdict, "-gren1", "102", nullptr); - FakeClientCommand(pBot->pEdict, "-gren2", "101", nullptr); - pBot->nadePrimed = false; - pBot->nadeType = 0; + BotReleasePrimedGrenade(pBot); pBot->tossNade = 1; pBot->f_move_speed = -pBot->f_max_speed; // back away from the throw job_ptr->phase = 2; @@ -3686,6 +3683,14 @@ int JobBinGrenade(bot_t* pBot) { // phase 1 - face where to throw the grenade if (job_ptr->phase == 1) { + // The per-frame safety deadline may have released the grenade before the + // bot finished turning. Stop aiming and use the ordinary retreat phase. + if (!pBot->nadePrimed) { + job_ptr->phase = 2; + job_ptr->phase_timer = pBot->f_think_time + 1.0f; + return JOB_UNDERWAY; + } + pBot->f_side_speed = 0.0f; pBot->f_move_speed = 0.0f; BotSetFacing(pBot, job_ptr->origin); @@ -3696,11 +3701,8 @@ int JobBinGrenade(bot_t* pBot) { if (BotInFieldOfView(pBot, v_aim) == 0 || timeToDet <= 1.2f) { // release the grenade now that we're facing the target if (pBot->nadePrimed) { - FakeClientCommand(pBot->pEdict, "-gren1", "102", nullptr); - FakeClientCommand(pBot->pEdict, "-gren2", "101", nullptr); - pBot->nadePrimed = false; - pBot->nadeType = 0; pBot->tossNade = 1; + BotReleasePrimedGrenade(pBot); } job_ptr->phase = 2; job_ptr->phase_timer = pBot->f_think_time + 1.0f; diff --git a/bot_job_think.cpp b/bot_job_think.cpp index 756f98c..96d1de0 100644 --- a/bot_job_think.cpp +++ b/bot_job_think.cpp @@ -242,11 +242,11 @@ job_struct* InitialiseNewJob(const bot_t* pBot, const int newJobType) { bool SubmitNewJob(bot_t* pBot, const int newJobType, job_struct* newJob) { int i; - // Direct support must work even when the bot temporarily has - // no current waypoint. JobBuffAlly can immediately handle a nearby visible - // patient without route data and will terminate safely if navigation is needed - // but no patient waypoint can be found. - if (pBot->current_wp == -1 && newJobType != JOB_SEEK_WAYPOINT && newJobType != JOB_BUFF_ALLY) // bit of a kludge but necessary + // Direct support and disposal of a live grenade must work even when the bot + // temporarily has no current waypoint. Both jobs can act on nearby state + // without route data and terminate safely if no useful waypoint can be found. + if (pBot->current_wp == -1 && newJobType != JOB_SEEK_WAYPOINT && + newJobType != JOB_BUFF_ALLY && newJobType != JOB_BIN_GRENADE) // bit of a kludge but necessary return false; // many job assessor functions need a valid current waypoint // if the job is currently blacklisted keep it out of the buffer