From b25f448d5c2bfff891531c766d40af433895d1a0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 23 Aug 2026 22:08:57 -0400 Subject: [PATCH] fix AI ships getting permanently stuck after losing or refusing a target Generalize the unreleased "$fix target updating with strafe:" ai_profiles flag into "$fix ai target recovery:", which now also covers two more ways a ship holding a standing chase order could park in AIM_NONE forever: * a ship already in AIM_NONE with a standing chase order (attack-any etc.) now clears its active goal so the order is re-processed, closing the window where retargeting finds a new enemy before the strafe clause can fire * target selection (find_enemy/get_nearest_objnum) now skips ships outside the attacker's actively-pursues list, and ai_chase() drops self-chosen targets it refuses, instead of holding a target it will never attack Retail behavior is unchanged when the flag is disabled; explicit orders on non-pursued targets are still declined as before. Co-Authored-By: Claude Fable 5 --- code/ai/ai_flags.h | 5 ++- code/ai/ai_profiles.cpp | 4 +-- code/ai/aicode.cpp | 76 +++++++++++++++++++++++++++++------------ code/ai/aicode.h | 0 code/ai/aigoals.h | 4 +++ 5 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 code/ai/aicode.h diff --git a/code/ai/ai_flags.h b/code/ai/ai_flags.h index 427ec5f8395..977dbffde7e 100644 --- a/code/ai/ai_flags.h +++ b/code/ai/ai_flags.h @@ -176,7 +176,10 @@ namespace AI { Dont_limit_change_in_speed_due_to_physics_whack, Guards_ignore_protected_attackers, Fix_standard_strafe, - Fix_target_updating_with_strafe, + Fix_ai_target_recovery, // a) strafing ships that lose their target re-process their orders, like chasing ships already do + // b) ships parked in AIM_NONE while holding a standing chase order (attack-any etc.) re-process their orders; + // c) target selection skips ships outside the attacker's actively-pursues list, and self-chosen targets + // outside that list are dropped rather than held forever Standard_strafe_used_more, Unify_usage_ai_shield_manage_delay, Fix_AI_shield_management_bug, diff --git a/code/ai/ai_profiles.cpp b/code/ai/ai_profiles.cpp index 9210d056774..30c9b5de33c 100644 --- a/code/ai/ai_profiles.cpp +++ b/code/ai/ai_profiles.cpp @@ -677,7 +677,7 @@ void parse_ai_profiles_tbl(const char *filename) set_flag(profile, "$fix standard strafe:", AI::Profile_Flags::Fix_standard_strafe); - set_flag(profile, "$fix target updating with strafe:", AI::Profile_Flags::Fix_target_updating_with_strafe); + set_flag(profile, "$fix ai target recovery:", AI::Profile_Flags::Fix_ai_target_recovery); set_flag(profile, "$standard strafe used more:", AI::Profile_Flags::Standard_strafe_used_more); @@ -1000,6 +1000,6 @@ void ai_profile_t::reset() flags.set(AI::Profile_Flags::Fix_shockwave_expire_before_do_damage); flags.set(AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired); flags.set(AI::Profile_Flags::Fix_standard_strafe); - flags.set(AI::Profile_Flags::Fix_target_updating_with_strafe); + flags.set(AI::Profile_Flags::Fix_ai_target_recovery); } } diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index a32e9ac3c93..1c3b6f6c809 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -2205,8 +2205,16 @@ int is_ignore_object(ai_info *aip, int objnum, int just_the_original = 0) return 0; } +// Returns true if a ship of class type attacker_class_type is willing to chase a target of class type target_class_type. +// Mirrors the actively-pursues fallbacks in ai_chase(): if either class type is unknown, the chase is allowed. +static bool ai_class_type_actively_pursues(int attacker_class_type, int target_class_type) +{ + if (attacker_class_type < 0 || target_class_type < 0) + return true; - + const auto &pursues = Ship_types[attacker_class_type].ai_actively_pursues; + return std::any_of(pursues.begin(), pursues.end(), [target_class_type](int pursued_type) { return pursued_type == target_class_type; }); +} typedef struct eval_nearest_objnum { int objnum; @@ -2214,6 +2222,7 @@ typedef struct eval_nearest_objnum { int enemy_team_mask; int enemy_ship_info_index; int enemy_class_type; + int attacker_class_type; int enemy_wing; float range; int max_attackers; @@ -2222,7 +2231,6 @@ typedef struct eval_nearest_objnum { int check_danger_weapon_objnum; } eval_nearest_objnum; - void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) { ai_info *aip; @@ -2273,6 +2281,13 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) float dist; int num_attacking; + // Don't pick a target that this ship's type refuses to chase, or it will park in AIM_NONE holding a target it never attacks. + // Only for unconstrained searches: an explicit chase-ship-class/type order should still produce a target even if ai_chase() doesn't chase it. + if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery] + && eno->enemy_ship_info_index < 0 && eno->enemy_class_type < 0 + && !ai_class_type_actively_pursues(eno->attacker_class_type, Ship_info[shipp->ship_info_index].class_type)) + return; + // Allow targeting of stealth in nebula by his firing at me // This is done for a specific ship, not generally. if ( !eno->check_danger_weapon_objnum ) { @@ -2352,6 +2367,7 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra eno.enemy_team_mask = enemy_team_mask; eno.enemy_ship_info_index = ship_info_index; eno.enemy_class_type = class_type; + eno.attacker_class_type = Ship_info[Ships[Objects[objnum].instance].ship_info_index].class_type; eno.enemy_wing = enemy_wing; eno.max_attackers = max_attackers; eno.objnum = objnum; @@ -2513,28 +2529,36 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, if (objnum < 0) return -1; + object* objp = &Objects[objnum]; - enemy_team_mask = iff_get_attackee_mask(obj_team(&Objects[objnum])); + enemy_team_mask = iff_get_attackee_mask(obj_team(objp)); // if target_objnum != -1, use that as goal. - ai_info* aip = &Ai_info[Ships[Objects[objnum].instance].ai_index]; + ship* shipp = &Ships[objp->instance]; + ai_info* aip = &Ai_info[shipp->ai_index]; if (timestamp_elapsed(aip->choose_enemy_timestamp)) { aip->choose_enemy_timestamp = timestamp(get_enemy_timestamp()); if (aip->target_objnum != -1) { int target_objnum = aip->target_objnum; + object* target_objp = &Objects[target_objnum]; // DKA don't undo object as target in nebula missions. // This could cause attack on ship on fringe on nebula to stop if attackee moves out of nebula range. (BAD) - if (Objects[target_objnum].signature == aip->target_signature) { - ship* target_shipp = (Objects[target_objnum].type == OBJ_SHIP) ? &Ships[Objects[target_objnum].instance] : nullptr; + if (target_objp->signature == aip->target_signature) { + ship* target_shipp = (target_objp->type == OBJ_SHIP) ? &Ships[target_objp->instance] : nullptr; if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) { if (ship_info_index < 0 || ship_info_index == target_shipp->ship_info_index) { - if (class_type < 0 || (target_shipp->ship_info_index >= 0 && - class_type == Ship_info[target_shipp->ship_info_index].class_type)) { - if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { - return target_objnum; + if (class_type < 0 || class_type == Ship_info[target_shipp->ship_info_index].class_type) { + if (!(target_objp->flags[Object::Object_Flags::Protected])) { + // same as get_nearest_objnum: only skip an unpursued target for unconstrained searches + if (!The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery] + || ship_info_index >= 0 || class_type >= 0 + || ai_class_type_actively_pursues(Ship_info[shipp->ship_info_index].class_type, + Ship_info[target_shipp->ship_info_index].class_type)) { + return target_objnum; + } } } } @@ -8986,15 +9010,8 @@ void ai_chase() ship_info *esip = &Ship_info[Ships[En_objp->instance].ship_info_index]; if (esip->class_type > -1) { - ship_type_info *stp = &Ship_types[sip->class_type]; - size_t ap_size = stp->ai_actively_pursues.size(); - for(size_t i = 0; i < ap_size; i++) - { - if(stp->ai_actively_pursues[i] == esip->class_type) { - go_after_it = true; - break; - } - } + const auto &pursues = Ship_types[sip->class_type].ai_actively_pursues; + go_after_it = std::any_of(pursues.begin(), pursues.end(), [esip](int pursued_type) { return pursued_type == esip->class_type; }); } else { @@ -9007,6 +9024,15 @@ void ai_chase() //WMC - Guess we do need this if (!go_after_it) { + // If we picked this target ourselves -- through auto-attack, dynamic chase, or a standing chase order -- + // drop it, so that the retargeting logic in ai_frame() can find something we are willing to chase. + if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]) { + if (aip->active_goal < 0 || aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC + || (aip->active_goal < MAX_AI_GOALS && ai_goal_is_standing_chase(aip->goals[aip->active_goal].ai_mode))) { + aip->target_objnum = -1; + aip->target_signature = -1; + } + } aip->mode = AIM_NONE; return; } @@ -15504,16 +15530,24 @@ void ai_frame(int objnum) En_objp = NULL; } - if (aip->mode == AIM_CHASE || ((The_mission.ai_profile->flags[AI::Profile_Flags::Fix_target_updating_with_strafe]) && aip->mode == AIM_STRAFE)) { + if (aip->mode == AIM_CHASE || ((The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]) && aip->mode == AIM_STRAFE)) { // If we're chasing or strafing against large ship but have lost our target, clear the active goal // to ensure ai_process_mission_orders() re-evaluates our orders next frame. // Without this fighter/bomber whose strafe target is destroyed drops to AIM_NONE (see ai_execute_behavior) // with its active_goal still set, so a standing order like attack-any is never re-processed - // and the ship slows to zero and remains still until another order is issued. + // and the ship slows to zero and remains still until another order is issued. if (En_objp == nullptr) { aip->active_goal = -1; } } + // Similarly, a ship that has already dropped to AIM_NONE while holding a standing chase order can never + // recover on its own: ai_execute_behavior() does nothing in AIM_NONE, and ai_process_mission_orders() + // returns early while active_goal is set. Clear the active goal so the order is re-processed next frame. + else if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery] && aip->mode == AIM_NONE + && aip->active_goal >= 0 && aip->active_goal < MAX_AI_GOALS + && ai_goal_is_standing_chase(aip->goals[aip->active_goal].ai_mode)) { + aip->active_goal = -1; + } // If there is a goal to resume and enough time has elapsed, resume the goal. if ((aip->resume_goal_time > 0) && (aip->resume_goal_time < Missiontime)) { diff --git a/code/ai/aicode.h b/code/ai/aicode.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/code/ai/aigoals.h b/code/ai/aigoals.h index d6a449d4c3b..9188fc689b9 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -106,6 +106,10 @@ inline bool ai_goal_is_specific_chase(ai_goal_mode ai_mode) { return ai_mode == AI_GOAL_CHASE || ai_mode == AI_GOAL_CHASE_WING || ai_mode == AI_GOAL_CHASE_SHIP_CLASS || ai_mode == AI_GOAL_CHASE_SHIP_TYPE; } +inline bool ai_goal_is_standing_chase(ai_goal_mode ai_mode) +{ + return ai_mode == AI_GOAL_CHASE_ANY; +} enum class ai_achievability { ACHIEVABLE, NOT_ACHIEVABLE, NOT_KNOWN, SATISFIED };