From 60c554354c0550fa6d93aa398aec62a1217bb407 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 21 Jul 2026 00:31:56 -0400 Subject: [PATCH] fix dual fire handling for banks that cannot use it The Secondary_dual_fire flag is per-ship, but not every secondary bank can dual fire. Previously the HUD drew the dual fire link icon even when the armed bank had only one firepoint, and firing such a bank cleared the flag entirely, silently discarding the player's dual fire preference for the other banks. Extend the ignored-rather-than-cleared approach already established for no-doublefire weapons to the single-firepoint case: - add ship_secondary_bank_can_dual_fire() as the single source of truth for bank validity, No_doublefire, ai_profiles restrictions, and firepoint count - ship_fire_secondary() now fires single and leaves the flag alone when the bank cannot dual fire, instead of removing the flag - the weapons gauges and the hardpoints gauge consult the helper, so the link icon and second-firepoint highlight are no longer drawn for incapable banks - the dual fire toggle on a single-firepoint bank now explains why it cannot dual fire and leaves the preference untouched - restoring the saved dual fire preference no longer depends on bank 0's firepoint count, since incapable banks now simply ignore the flag - key control and force feedback also consult the helper Co-Authored-By: Claude Fable 5 --- code/hud/hudtarget.cpp | 16 ++++----- code/io/keycontrol.cpp | 17 ++++++---- code/localization/localize.cpp | 2 +- code/playerman/playercontrol.cpp | 5 +-- code/ship/ship.cpp | 57 +++++++++++++++++++++----------- code/ship/ship.h | 1 + code/weapon/weapons.cpp | 12 +++++-- 7 files changed, 68 insertions(+), 42 deletions(-) diff --git a/code/hud/hudtarget.cpp b/code/hud/hudtarget.cpp index 6d2c7711582..7a29f60ea3a 100644 --- a/code/hud/hudtarget.cpp +++ b/code/hud/hudtarget.cpp @@ -6775,10 +6775,9 @@ void HudGaugeWeapons::render(float /*frametime*/, bool config) renderPrintfWithGauge(x + fl2i(Weapon_sunlinked_offset_x * scale), name_y, EG_NULL, scale, config, "%c", Weapon_link_icon); // indicate if this is linked - // don't draw the link indicator if the fire can't be fired link. - // the link flag is ignored rather than cleared so the player can cycle past a no-doublefire weapon without the setting being cleared - if (!config && Player_ship->flags[Ship::Ship_Flags::Secondary_dual_fire] && !wip->wi_flags[Weapon::Info_Flags::No_doublefire] && - !The_mission.ai_profile->flags[AI::Profile_Flags::Disable_player_secondary_doublefire] ) { + // don't draw the link indicator if this bank can't actually fire linked. + // the link flag is ignored rather than cleared so the player can cycle past an incapable bank or weapon without the setting being cleared + if (!config && Player_ship->flags[Ship::Ship_Flags::Secondary_dual_fire] && ship_secondary_bank_can_dual_fire(Player_ship, i)) { renderPrintfWithGauge(x + fl2i(Weapon_slinked_offset_x * scale), name_y, EG_NULL, scale, config, "%c", Weapon_link_icon); } @@ -7732,10 +7731,9 @@ void HudGaugeSecondaryWeapons::render(float /*frametime*/, bool config) renderPrintfWithGauge(position[0] + _sunlinked_offset_x, position[1] + text_y_offset, EG_NULL, 1.0f, config, "%c", Weapon_link_icon); // indicate if this is linked - // don't draw the link indicator if the fire can't be fired link. - // the link flag is ignored rather than cleared so the player can cycle past a no-doublefire weapon without the setting being cleared - if ( Player_ship->flags[Ship::Ship_Flags::Secondary_dual_fire] && !wip->wi_flags[Weapon::Info_Flags::No_doublefire] && - !The_mission.ai_profile->flags[AI::Profile_Flags::Disable_player_secondary_doublefire] ) { + // don't draw the link indicator if this bank can't actually fire linked. + // the link flag is ignored rather than cleared so the player can cycle past an incapable bank or weapon without the setting being cleared + if ( Player_ship->flags[Ship::Ship_Flags::Secondary_dual_fire] && ship_secondary_bank_can_dual_fire(Player_ship, i) ) { renderPrintfWithGauge(position[0] + _slinked_offset_x, position[1] + text_y_offset, EG_NULL, 1.0f, config, "%c", Weapon_link_icon); } @@ -7945,7 +7943,7 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) model_render_params weapon_render_info; - if ( swp->current_secondary_bank == i && ( swp->secondary_next_slot[i] == k || ( swp->secondary_next_slot[i]+1 == k && sp->flags[Ship::Ship_Flags::Secondary_dual_fire] ) ) ) { + if ( swp->current_secondary_bank == i && ( swp->secondary_next_slot[i] == k || ( swp->secondary_next_slot[i]+1 == k && sp->flags[Ship::Ship_Flags::Secondary_dual_fire] && ship_secondary_bank_can_dual_fire(sp, i) ) ) ) { weapon_render_info.set_color(Color_bright_blue); } else { weapon_render_info.set_color(Color_bright_white); diff --git a/code/io/keycontrol.cpp b/code/io/keycontrol.cpp index b72f9807729..049833e4bd7 100644 --- a/code/io/keycontrol.cpp +++ b/code/io/keycontrol.cpp @@ -1931,19 +1931,22 @@ int button_function_critical(int n, net_player *p = NULL) break; } - polymodel *pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num); - - int firepoints = pm->missile_banks[Ships[objp->instance].weapons.current_secondary_bank].num_slots; - - if (Ships[objp->instance].flags[Ship::Ship_Flags::Secondary_dual_fire] || firepoints < 2) { - Ships[objp->instance].flags.remove(Ship::Ship_Flags::Secondary_dual_fire); + if (!ship_secondary_bank_can_dual_fire(&Ships[objp->instance], Ships[objp->instance].weapons.current_secondary_bank)) { + // leave the dual fire preference alone; it is ignored rather than cleared for banks that can't use it + if(at_self) { + HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR( "This secondary bank cannot fire dual missiles", 1932)); + gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL); + hud_gauge_popup_start(HUD_WEAPONS_GAUGE); + } + } else if (Ships[objp->instance].flags[Ship::Ship_Flags::Secondary_dual_fire]) { + Ships[objp->instance].flags.remove(Ship::Ship_Flags::Secondary_dual_fire); if(at_self) { HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR( "Secondary weapon set to normal fire mode", 34)); snd_play( gamesnd_get_game_sound(ship_get_sound(Player_obj, GameSounds::SECONDARY_CYCLE)) ); hud_gauge_popup_start(HUD_WEAPONS_GAUGE); } } else { - Ships[objp->instance].flags.set(Ship::Ship_Flags::Secondary_dual_fire); + Ships[objp->instance].flags.set(Ship::Ship_Flags::Secondary_dual_fire); if(at_self) { HUD_sourced_printf(HUD_SOURCE_HIDDEN, "%s", XSTR( "Secondary weapon set to dual fire mode", 35)); snd_play( gamesnd_get_game_sound(ship_get_sound(Player_obj, GameSounds::SECONDARY_CYCLE)) ); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 12a8d34ca2b..6565e45022a 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -64,7 +64,7 @@ bool *Lcl_unexpected_tstring_check = nullptr; // NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous, // but internal strings should still increment XSTR_SIZE to avoid collisions. // retail XSTR_SIZE = 1570 -// #define XSTR_SIZE 1932 // This is the next available ID +// #define XSTR_SIZE 1933 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/playerman/playercontrol.cpp b/code/playerman/playercontrol.cpp index e023aa7e9f7..5d9894964be 100644 --- a/code/playerman/playercontrol.cpp +++ b/code/playerman/playercontrol.cpp @@ -1420,7 +1420,6 @@ void player_restore_target_and_weapon_link_prefs() { ship_info *player_sip; player_sip = &Ship_info[Player_ship->ship_info_index]; - polymodel *pm = model_get(player_sip->model_num); // Don't restores the save flags in training, as we must ensure certain things are off, such as speed matching. if ( !(The_mission.game_type & MISSION_TYPE_TRAINING )) { @@ -1433,7 +1432,9 @@ void player_restore_target_and_weapon_link_prefs() } } - if ( Player->flags & PLAYER_FLAGS_LINK_SECONDARY && (pm->n_missiles > 0 && pm->missile_banks[0].num_slots > 1) ) { + // restore the dual fire preference regardless of whether the current bank can use it; + // the flag is ignored rather than cleared for banks that can't + if ( Player->flags & PLAYER_FLAGS_LINK_SECONDARY ) { Player_ship->flags.set(Ship::Ship_Flags::Secondary_dual_fire); } } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index a18ada69b4c..08b06de480f 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -14259,6 +14259,37 @@ static bool ship_fire_secondary_detonate(object *obj, ship_weapon *swp) extern void ai_maybe_announce_shockwave_weapon(object *firing_objp, int weapon_index); +// Determines whether this secondary bank is currently capable of dual fire. Note that the +// Secondary_dual_fire flag can legitimately be set even when this returns false: the flag is +// ignored rather than cleared so that the player's dual fire preference isn't lost when +// cycling through banks or weapons. All code that acts on the flag should check this too. +bool ship_secondary_bank_can_dual_fire(const ship *shipp, int bank) +{ + if (bank < 0 || bank >= shipp->weapons.num_secondary_banks) + return false; + + int weapon_class = shipp->weapons.secondary_bank_weapons[bank]; + if (weapon_class < 0) + return false; + + if (Weapon_info[weapon_class].wi_flags[Weapon::Info_Flags::No_doublefire]) + return false; + + if (shipp->objnum == OBJ_INDEX(Player_obj)) + { + if (The_mission.ai_profile->flags[AI::Profile_Flags::Disable_player_secondary_doublefire]) + return false; + } + else + { + if (The_mission.ai_profile->flags[AI::Profile_Flags::Disable_ai_secondary_doublefire]) + return false; + } + + polymodel *pm = model_get(Ship_info[shipp->ship_info_index].model_num); + return pm->missile_banks[bank].num_slots > 1; +} + // Object *obj fires its secondary weapon, if it can. // If its most recently fired weapon is a remotely detonatable weapon, detonate it. // Returns number of weapons fired. Note, for swarmers, returns 1 if it is allowed @@ -14621,25 +14652,14 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) goto done_secondary; } - // Handle the optional disabling of dual fire - // dual fire/doublefire can be disabled for individual weapons, for players, or for AIs - // if any of these apply to the current weapon, unset the dual fire flag on the ship - // then proceed as normal. - // This is only handled at firing time so dualfire isn't lost when cycling through weapons - if (shipp->flags[Ship_Flags::Secondary_dual_fire] && - ( wip->wi_flags[Weapon::Info_Flags::No_doublefire] || - ( The_mission.ai_profile->flags[AI::Profile_Flags::Disable_ai_secondary_doublefire] && - shipp->objnum != OBJ_INDEX(Player_obj) ) || - ( The_mission.ai_profile->flags[AI::Profile_Flags::Disable_player_secondary_doublefire] && - shipp->objnum == OBJ_INDEX(Player_obj) )) - ) { - shipp->flags.remove(Ship_Flags::Secondary_dual_fire); - } - int start_slot, end_slot; no_energy = shipp->weapon_energy < 2 * wip->energy_consumed; // whether there's enough energy for at least 1 shot was checked above - if ( shipp->flags[Ship_Flags::Secondary_dual_fire] && num_slots > 1) { + // Dual fire can be unavailable for this bank because it has only one firepoint, because + // the weapon disallows it, or because of an ai_profiles restriction. In any of these + // cases the flag is ignored rather than cleared so the dual fire preference isn't lost + // when cycling through banks or weapons. + if ( shipp->flags[Ship_Flags::Secondary_dual_fire] && ship_secondary_bank_can_dual_fire(shipp, bank) ) { start_slot = swp->secondary_next_slot[bank]; // AL 11-19-97: Ensure enough ammo remains when firing linked secondary weapons if ( check_ammo && ((swp->secondary_bank_ammo[bank] < 2 && !no_ammo_needed) || no_energy) ) { @@ -14648,9 +14668,6 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) end_slot = start_slot+1; } } else { - // de-set the flag just in case dual-fire was set but couldn't be used - // because there's less than two firepoints - shipp->flags.remove(Ship_Flags::Secondary_dual_fire); start_slot = swp->secondary_next_slot[bank]; end_slot = start_slot; } @@ -14779,7 +14796,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) swp = &Player_ship->weapons; if (bank >= 0) { wip = &Weapon_info[swp->secondary_bank_weapons[bank]]; - if (Player_ship->flags[Ship_Flags::Secondary_dual_fire]){ + if (Player_ship->flags[Ship_Flags::Secondary_dual_fire] && ship_secondary_bank_can_dual_fire(Player_ship, bank)){ joy_ff_play_secondary_shoot((int) (wip->cargo_size * 2.0f)); } else { joy_ff_play_secondary_shoot((int) wip->cargo_size); diff --git a/code/ship/ship.h b/code/ship/ship.h index 636084b9186..e76bde3580c 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1734,6 +1734,7 @@ extern bool in_autoaim_fov(ship *shipp, int bank_to_fire, object *obj); extern int ship_stop_fire_primary(object * obj); extern int ship_fire_primary(object * objp, int force = 0, bool rollback_shot = false); extern int ship_fire_secondary(object * objp, int allow_swarm = 0, bool rollback_shot = false ); +extern bool ship_secondary_bank_can_dual_fire(const ship *shipp, int bank); bool ship_start_secondary_fire(object* objp); bool ship_stop_secondary_fire(object* objp); extern int ship_launch_countermeasure(object *objp, int rand_val = -1); diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index c0a223459ea..e0026086375 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -7148,9 +7148,15 @@ int weapon_create( const vec3d *pos, const matrix *porient, int weapon_type, int Assertion( position != NULL, "'%s' is trying to fire a weapon that is not selected", Ships[parent_objp->instance].ship_name ); size_t curr_pos = *position; - if (((Weapon_info[weapon_type].subtype == WP_LASER && parent_shipp->flags[Ship::Ship_Flags::Primary_linked]) || - (Weapon_info[weapon_type].subtype == WP_MISSILE && parent_shipp->flags[Ship::Ship_Flags::Secondary_dual_fire])) && - (curr_pos > 0)) { + // only advance the pattern once per linked/paired shot rather than once per projectile + bool paired_shot = false; + if (Weapon_info[weapon_type].subtype == WP_LASER) { + paired_shot = parent_shipp->flags[Ship::Ship_Flags::Primary_linked]; + } else if (Weapon_info[weapon_type].subtype == WP_MISSILE && !src_turret && parent_shipp->flags[Ship::Ship_Flags::Secondary_dual_fire]) { + // for dual fire, also check whether the armed bank can actually use it, since the flag is ignored rather than cleared for banks that can't + paired_shot = ship_secondary_bank_can_dual_fire(parent_shipp, parent_shipp->weapons.current_secondary_bank); + } + if (paired_shot && (curr_pos > 0)) { curr_pos--; } ++(*position);