From ddbe28b69141dc74f6fadf1e059e6d19791a10a3 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Jul 2026 14:08:38 -0400 Subject: [PATCH 1/5] consolidate per-bank external weapon model state into one struct The state for a bank's external weapon model was scattered across two structs and three naming schemes: the model instance and its weapon in ship_weapon, the firing-point chain counter in a combined array indexed with the bank + MAX_SHIP_PRIMARY_BANKS packing convention, and the gatling spin rate/angle on ship itself. Gather them into external_weapon_state, with one array for primary banks and one for secondary banks in ship_weapon. This removes the index-packing convention (ship_get_external_model_fp_offset now takes the bank's state directly), and ship_delete now also frees secondary instances should a future weapon ever create one. No behavior change. Co-Authored-By: Claude Fable 5 --- code/ai/aicode.cpp | 4 +- code/ship/ship.cpp | 106 ++++++++++++++++++++++++--------------------- code/ship/ship.h | 20 ++++++--- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index efae89f7b0b..728752675c5 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -6787,8 +6787,8 @@ bool check_los(int objnum, int target_objnum, float threshold, int primary_bank, } // use the same firing point the next shot will use - int external_bank = is_primary ? primary_bank : secondary_bank + MAX_SHIP_PRIMARY_BANKS; - vec3d external_fp_offset = ship_get_external_model_fp_offset(swp, wip, weapon_model, external_bank, false); + auto ext = is_primary ? &swp->primary_bank_external_weapon[primary_bank] : &swp->secondary_bank_external_weapon[secondary_bank]; + vec3d external_fp_offset = ship_get_external_model_fp_offset(ext, wip, weapon_model, false); vm_vec_add2(&pnt, &external_fp_offset); vm_vec_unrotate(&firing_point, &pnt, &firing_ship->orient); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 750e8589871..c87707d2f05 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -6922,9 +6922,6 @@ void ship::clear() // ---------- special weapons init that isn't setting things to 0 for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - // not part of weapons! - primary_rotate_rate[i] = 0.0f; - primary_rotate_ang[i] = 0.0f; // for fighter beams was_firing_last_frame[i] = 0; } @@ -7130,8 +7127,7 @@ void ship_weapon::clear() { primary_bank_weapons[i] = -1; - primary_bank_external_model_instance[i] = -1; - primary_bank_model_instance_weapon[i] = -1; + primary_bank_external_weapon[i] = external_weapon_state(); next_primary_fire_stamp[i] = timestamp(0); last_primary_fire_stamp[i] = timestamp(-1); @@ -7153,7 +7149,6 @@ void ship_weapon::clear() burst_counter[i] = 0; burst_seed[i] = Random::next(); - external_model_fp_counter[i] = 0; primary_firepoint_indices[i].clear(); primary_firepoint_next_to_fire_index[i] = 0; @@ -7165,6 +7160,8 @@ void ship_weapon::clear() { secondary_bank_weapons[i] = -1; + secondary_bank_external_weapon[i] = external_weapon_state(); + next_secondary_fire_stamp[i] = timestamp(0); last_secondary_fire_stamp[i] = timestamp(-1); @@ -7181,7 +7178,6 @@ void ship_weapon::clear() secondary_bank_substitution_pattern_index[i] = 0; burst_counter[i + MAX_SHIP_PRIMARY_BANKS] = 0; - external_model_fp_counter[i + MAX_SHIP_PRIMARY_BANKS] = 0; } tertiary_bank_ammo = 0; @@ -8630,12 +8626,20 @@ void ship_delete( object * obj ) shipp->model_instance_num = -1; // free up any weapon model instances - for (int i = 0; i < shipp->weapons.num_primary_banks; ++i) + for (auto &ext : shipp->weapons.primary_bank_external_weapon) + { + if (ext.model_instance >= 0) + { + model_delete_instance(ext.model_instance); + ext.model_instance = -1; + } + } + for (auto &ext : shipp->weapons.secondary_bank_external_weapon) { - if (shipp->weapons.primary_bank_external_model_instance[i] >= 0) + if (ext.model_instance >= 0) { - model_delete_instance(shipp->weapons.primary_bank_external_model_instance[i]); - shipp->weapons.primary_bank_external_model_instance[i] = -1; + model_delete_instance(ext.model_instance); + ext.model_instance = -1; } } } @@ -12593,16 +12597,18 @@ static int ship_stop_fire_primary_bank(object * obj, int bank_to_stop) swp = &shipp->weapons; if(swp->primary_bank_weapons[bank_to_stop] >= 0){ - if(shipp->primary_rotate_rate[bank_to_stop] > 0.0f) - shipp->primary_rotate_rate[bank_to_stop] -= Weapon_info[swp->primary_bank_weapons[bank_to_stop]].weapon_submodel_rotate_accell*flFrametime; - if(shipp->primary_rotate_rate[bank_to_stop] < 0.0f) - shipp->primary_rotate_rate[bank_to_stop] = 0.0f; + auto &ext = swp->primary_bank_external_weapon[bank_to_stop]; + + if(ext.rotate_rate > 0.0f) + ext.rotate_rate -= Weapon_info[swp->primary_bank_weapons[bank_to_stop]].weapon_submodel_rotate_accell*flFrametime; + if(ext.rotate_rate < 0.0f) + ext.rotate_rate = 0.0f; if(Ship_info[shipp->ship_info_index].draw_primary_models[bank_to_stop]){ - shipp->primary_rotate_ang[bank_to_stop] += shipp->primary_rotate_rate[bank_to_stop]*flFrametime; - if(shipp->primary_rotate_ang[bank_to_stop] > PI2) - shipp->primary_rotate_ang[bank_to_stop] -= PI2; - if(shipp->primary_rotate_ang[bank_to_stop] < 0.0f) - shipp->primary_rotate_ang[bank_to_stop] += PI2; + ext.rotate_ang += ext.rotate_rate*flFrametime; + if(ext.rotate_ang > PI2) + ext.rotate_ang -= PI2; + if(ext.rotate_ang < 0.0f) + ext.rotate_ang += PI2; } } @@ -12739,10 +12745,9 @@ bool in_autoaim_fov(ship *shipp, int bank_to_fire, object *obj) // weapon flag: chained weapons cycle through the external model's firing points using the bank's // chain counter; non-chained weapons use the sub_shot index directly. // -// fp_counter_index indexes swp->external_model_fp_counter[]: the bank number for primaries, or -// the bank number plus MAX_SHIP_PRIMARY_BANKS for secondaries. advance_counter should be true -// when actually firing a shot, and false for queries such as the AI's line-of-sight check. -vec3d ship_get_external_model_fp_offset(ship_weapon *swp, const weapon_info *wip, const polymodel *weapon_model, int fp_counter_index, bool advance_counter, int sub_shot) +// ext is the bank's external weapon state. advance_counter should be true when actually firing +// a shot, and false for queries such as the AI's line-of-sight check. +vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, bool advance_counter, int sub_shot) { if (weapon_model == nullptr || weapon_model->n_guns < 1 || weapon_model->gun_banks[0].num_slots < 1) return vmd_zero_vector; @@ -12751,15 +12756,13 @@ vec3d ship_get_external_model_fp_offset(ship_weapon *swp, const weapon_info *wip if (wip->wi_flags[Weapon::Info_Flags::External_weapon_fp]) { - int &fp_counter = swp->external_model_fp_counter[fp_counter_index]; - // the counter cycles through the firing points of the model's first gun bank - if (fp_counter < 0 || fp_counter >= bank.num_slots) - fp_counter = 0; + if (ext->fp_counter < 0 || ext->fp_counter >= bank.num_slots) + ext->fp_counter = 0; - int fp = fp_counter; + int fp = ext->fp_counter; if (advance_counter) - fp_counter++; + ext->fp_counter++; return bank.pnt[fp]; } @@ -12926,18 +12929,20 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) } if (winfo_p->weapon_submodel_rotate_vel > 0.0f) { - if (shipp->primary_rotate_rate[bank_to_fire] < winfo_p->weapon_submodel_rotate_vel) - shipp->primary_rotate_rate[bank_to_fire] += winfo_p->weapon_submodel_rotate_accell*flFrametime; - if (shipp->primary_rotate_rate[bank_to_fire] > winfo_p->weapon_submodel_rotate_vel) - shipp->primary_rotate_rate[bank_to_fire] = winfo_p->weapon_submodel_rotate_vel; + auto &ext = swp->primary_bank_external_weapon[bank_to_fire]; + + if (ext.rotate_rate < winfo_p->weapon_submodel_rotate_vel) + ext.rotate_rate += winfo_p->weapon_submodel_rotate_accell*flFrametime; + if (ext.rotate_rate > winfo_p->weapon_submodel_rotate_vel) + ext.rotate_rate = winfo_p->weapon_submodel_rotate_vel; if (sip->draw_primary_models[bank_to_fire]) { - shipp->primary_rotate_ang[bank_to_fire] += shipp->primary_rotate_rate[bank_to_fire]*flFrametime; - if (shipp->primary_rotate_ang[bank_to_fire] > PI2) - shipp->primary_rotate_ang[bank_to_fire] -= PI2; - if (shipp->primary_rotate_ang[bank_to_fire] < 0.0f) - shipp->primary_rotate_ang[bank_to_fire] += PI2; + ext.rotate_ang += ext.rotate_rate*flFrametime; + if (ext.rotate_ang > PI2) + ext.rotate_ang -= PI2; + if (ext.rotate_ang < 0.0f) + ext.rotate_ang += PI2; } - if (shipp->primary_rotate_rate[bank_to_fire] < winfo_p->weapon_submodel_rotate_vel) + if (ext.rotate_rate < winfo_p->weapon_submodel_rotate_vel) continue; } // if this is a targeting laser, start it up ///- only targeting laser if it is tag-c, otherwise it's a fighter beam -Bobboau @@ -13454,7 +13459,7 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) vec3d dir; dir = pm->gun_banks[bank_to_fire].norm[pt]; - vec3d external_fp_offset = ship_get_external_model_fp_offset(swp, winfo_p, weapon_model, bank_to_fire, true, s); + vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->primary_bank_external_weapon[bank_to_fire], winfo_p, weapon_model, true, s); vm_vec_add2(&pnt, &external_fp_offset); vm_vec_unrotate(&gun_point, &pnt, &obj->orient); @@ -14316,7 +14321,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) } // chained weapons cycle through the external model's firing points; other weapons use the 0 index slot - vec3d external_fp_offset = ship_get_external_model_fp_offset(swp, wip, weapon_model, bank + MAX_SHIP_PRIMARY_BANKS, true); + vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->secondary_bank_external_weapon[bank], wip, weapon_model, true); vm_vec_add2(&pnt, &external_fp_offset); vm_vec_unrotate(&missile_point, &pnt, &obj->orient); vm_vec_add(&firing_pos, &missile_point, &obj->pos); @@ -21481,14 +21486,15 @@ void ship_render_batch_thrusters(object *obj) int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank) { int weapon_idx = swp->primary_bank_weapons[bank]; + auto &ext = swp->primary_bank_external_weapon[bank]; - if (swp->primary_bank_model_instance_weapon[bank] != weapon_idx) + if (ext.model_instance_weapon != weapon_idx) { // the weapon changed, so any existing instance belongs to the old weapon's model - if (swp->primary_bank_external_model_instance[bank] >= 0) + if (ext.model_instance >= 0) { - model_delete_instance(swp->primary_bank_external_model_instance[bank]); - swp->primary_bank_external_model_instance[bank] = -1; + model_delete_instance(ext.model_instance); + ext.model_instance = -1; } if (weapon_idx >= 0 && Weapon_info[weapon_idx].external_model_num >= 0) @@ -21500,16 +21506,16 @@ int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank) { if (pm->submodel[mn].flags[Model::Submodel_flags::Gun_rotation]) { - swp->primary_bank_external_model_instance[bank] = model_create_instance(model_objnum_special::OBJNUM_NONE, Weapon_info[weapon_idx].external_model_num); + ext.model_instance = model_create_instance(model_objnum_special::OBJNUM_NONE, Weapon_info[weapon_idx].external_model_num); break; } } } - swp->primary_bank_model_instance_weapon[bank] = weapon_idx; + ext.model_instance_weapon = weapon_idx; } - return swp->primary_bank_external_model_instance[bank]; + return ext.model_instance; } // Computes the position and orientation, in the ship model's frame, at which to render an @@ -21577,7 +21583,7 @@ void ship_render_weapon_models(model_render_params *ship_render_info, model_draw if (pm->submodel[mn].flags[Model::Submodel_flags::Gun_rotation]) { angles angs = vmd_zero_angles; - angs.b = shipp->primary_rotate_ang[i]; + angs.b = swp->primary_bank_external_weapon[i].rotate_ang; vm_angles_2_matrix(&pmi->submodel[mn].canonical_orient, &angs); } } diff --git a/code/ship/ship.h b/code/ship/ship.h index 6f38e0eda3f..7c99424674a 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -93,6 +93,17 @@ struct reinforcements { reinforcements(const char *reinforcement_name = nullptr); }; +// Per-bank state for a weapon's external model - the model rendered at the bank's firing +// points when the ship enables $Show Primary Models: / $Show Secondary Models:. +struct external_weapon_state +{ + int model_instance = -1; // model instance used to spin Gun_rotation submodels, or -1 if the weapon doesn't need one + int model_instance_weapon = -1; // the weapon the model instance state was created for, or -1 if not yet checked + int fp_counter = 0; // cycles through the model's firing points, for "chain external model fps" weapons + float rotate_rate = 0.0f; // current spin rate of the model's Gun_rotation submodels (primaries only) + float rotate_ang = 0.0f; // current spin angle of the model's Gun_rotation submodels (primaries only) +}; + class ship_weapon { public: int num_primary_banks; // Number of primary banks (same as model) @@ -102,8 +113,8 @@ class ship_weapon { int primary_bank_weapons[MAX_SHIP_PRIMARY_BANKS]; // Weapon_info[] index for the weapon in the bank int secondary_bank_weapons[MAX_SHIP_SECONDARY_BANKS]; // Weapon_info[] index for the weapon in the bank - int primary_bank_external_model_instance[MAX_SHIP_PRIMARY_BANKS]; - int primary_bank_model_instance_weapon[MAX_SHIP_PRIMARY_BANKS]; // the weapon the model instance was created for, or -1 if not yet checked + external_weapon_state primary_bank_external_weapon[MAX_SHIP_PRIMARY_BANKS]; + external_weapon_state secondary_bank_external_weapon[MAX_SHIP_SECONDARY_BANKS]; int current_primary_bank; // currently selected primary bank int current_secondary_bank; // currently selected secondary bank @@ -162,7 +173,6 @@ class ship_weapon { int burst_counter[MAX_SHIP_PRIMARY_BANKS + MAX_SHIP_SECONDARY_BANKS]; int burst_seed[MAX_SHIP_PRIMARY_BANKS + MAX_SHIP_SECONDARY_BANKS]; // A random seed, recalculated only when the weapon's burst resets - int external_model_fp_counter[MAX_SHIP_PRIMARY_BANKS + MAX_SHIP_SECONDARY_BANKS]; SCP_vector primary_firepoint_indices[MAX_SHIP_PRIMARY_BANKS]; // A list of firepoint indices which is shuffled for random fire ordering int primary_firepoint_next_to_fire_index[MAX_SHIP_PRIMARY_BANKS]; // For cycle firing modes, keeps track of which firepoint we're on @@ -863,8 +873,6 @@ class ship int bay_doors_parent_shipnum; // our parent ship, what we are entering/leaving reload_pct secondary_point_reload_pct; //after fireing a secondary it takes some time for that secondary weapon to reload, this is how far along in that proces it is (from 0 to 1) - float primary_rotate_rate[MAX_SHIP_PRIMARY_BANKS]; - float primary_rotate_ang[MAX_SHIP_PRIMARY_BANKS]; SCP_vector> rcs_activity; //Timestamp of when thrusters started //Sound index for thrusters @@ -1813,7 +1821,7 @@ extern void ship_actually_depart(int shipnum, int method = SHIP_DEPARTED_WARP); 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 vec3d ship_get_external_model_fp_offset(ship_weapon *swp, const weapon_info *wip, const polymodel *weapon_model, int fp_counter_index, bool advance_counter, int sub_shot = 0); +extern vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, bool advance_counter, int sub_shot = 0); extern void ship_get_weapon_model_slot_transform(const w_bank *bank, int slot, float reload_slide_back, vec3d *outpnt, matrix *outorient); extern int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank); extern int ship_fire_secondary(object * objp, int allow_swarm = 0, bool rollback_shot = false ); From d05ca940b5bb3e3ecc0cec7fbaf4c0447804c2c7 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Jul 2026 14:12:21 -0400 Subject: [PATCH 2/5] update external weapon model spin in per-frame processing The gatling spin state was integrated inside ship_fire_primary (spin-up) and ship_stop_fire_primary_bank (spin-down), which entangled a per-frame visual/state update with the firing event paths and had two quirks: - Spin-down only ran when something called the stop-fire path, so a bank could freeze mid-spin (e.g. AI ships that stop calling ship_fire_primary without going through ship_stop_fire_primary). - The spin angle only advanced when the ship class set $Show Primary Models:, so identical weapon state evolved differently per ship class even though the rate (which gates firing) always ran. Banks that try to fire now just request spin-up, and update_external_weapon_spin() -- called from ship_process_post next to update_reload_percent(), which was moved out of the physics code for the same reason -- integrates the rate and angle every frame. The firing gate (no shots until the barrels reach full speed) is unchanged. Co-Authored-By: Claude Fable 5 --- code/ship/ship.cpp | 77 +++++++++++++++++++++++++++++----------------- code/ship/ship.h | 1 + 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index c87707d2f05..e6f95bb4bbc 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -10609,6 +10609,49 @@ void update_firing_sounds(object* objp, ship* shipp) } } +// Spins the Gun_rotation submodels of external weapon models up or down. A bank that tried to +// fire this frame requests spin-up (see ship_fire_primary, which also refuses to fire until the +// barrels are up to speed); all other banks wind down. Only primary banks spin. +void update_external_weapon_spin(ship *shipp, float frametime) +{ + ship_weapon *swp = &shipp->weapons; + + for (int i = 0; i < swp->num_primary_banks; i++) + { + auto &ext = swp->primary_bank_external_weapon[i]; + + if (swp->primary_bank_weapons[i] < 0) + { + ext.rotate_rate = 0.0f; + ext.spin_up_requested = false; + continue; + } + + auto wip = &Weapon_info[swp->primary_bank_weapons[i]]; + + if (ext.spin_up_requested && wip->weapon_submodel_rotate_vel > 0.0f) + { + ext.rotate_rate += wip->weapon_submodel_rotate_accell * frametime; + if (ext.rotate_rate > wip->weapon_submodel_rotate_vel) + ext.rotate_rate = wip->weapon_submodel_rotate_vel; + } + else if (ext.rotate_rate > 0.0f) + { + ext.rotate_rate -= wip->weapon_submodel_rotate_accell * frametime; + if (ext.rotate_rate < 0.0f) + ext.rotate_rate = 0.0f; + } + ext.spin_up_requested = false; + + if (ext.rotate_rate > 0.0f) + { + ext.rotate_ang += ext.rotate_rate * frametime; + while (ext.rotate_ang > PI2) + ext.rotate_ang -= PI2; + } + } +} + // This was previously part of obj_move_call_physics(), but secondary_point_reload_pct is only used for rendering and has nothing to do with physics at all. void update_reload_percent(ship *shipp, float frametime) { @@ -10695,6 +10738,8 @@ void ship_process_post(object * obj, float frametime) update_reload_percent(shipp, frametime); + update_external_weapon_spin(shipp, frametime); + ship_dying_frame(obj, num); ship_move_ets_transfer_buffers(shipp, obj, frametime); @@ -12583,7 +12628,6 @@ DCF(t_max, "") static int ship_stop_fire_primary_bank(object * obj, int bank_to_stop) { ship *shipp; - ship_weapon *swp; if(obj == NULL){ return 0; @@ -12594,24 +12638,9 @@ static int ship_stop_fire_primary_bank(object * obj, int bank_to_stop) } shipp = &Ships[obj->instance]; - swp = &shipp->weapons; - if(swp->primary_bank_weapons[bank_to_stop] >= 0){ - auto &ext = swp->primary_bank_external_weapon[bank_to_stop]; + // (external weapon model spin-down is handled by update_external_weapon_spin) - if(ext.rotate_rate > 0.0f) - ext.rotate_rate -= Weapon_info[swp->primary_bank_weapons[bank_to_stop]].weapon_submodel_rotate_accell*flFrametime; - if(ext.rotate_rate < 0.0f) - ext.rotate_rate = 0.0f; - if(Ship_info[shipp->ship_info_index].draw_primary_models[bank_to_stop]){ - ext.rotate_ang += ext.rotate_rate*flFrametime; - if(ext.rotate_ang > PI2) - ext.rotate_ang -= PI2; - if(ext.rotate_ang < 0.0f) - ext.rotate_ang += PI2; - } - } - if(shipp->was_firing_last_frame[bank_to_stop] == 0) return 0; @@ -12931,17 +12960,9 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) if (winfo_p->weapon_submodel_rotate_vel > 0.0f) { auto &ext = swp->primary_bank_external_weapon[bank_to_fire]; - if (ext.rotate_rate < winfo_p->weapon_submodel_rotate_vel) - ext.rotate_rate += winfo_p->weapon_submodel_rotate_accell*flFrametime; - if (ext.rotate_rate > winfo_p->weapon_submodel_rotate_vel) - ext.rotate_rate = winfo_p->weapon_submodel_rotate_vel; - if (sip->draw_primary_models[bank_to_fire]) { - ext.rotate_ang += ext.rotate_rate*flFrametime; - if (ext.rotate_ang > PI2) - ext.rotate_ang -= PI2; - if (ext.rotate_ang < 0.0f) - ext.rotate_ang += PI2; - } + // spin up the Gun_rotation submodels (see update_external_weapon_spin), and + // don't fire until the barrels are up to speed + ext.spin_up_requested = true; if (ext.rotate_rate < winfo_p->weapon_submodel_rotate_vel) continue; } diff --git a/code/ship/ship.h b/code/ship/ship.h index 7c99424674a..87aebe33bd6 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -102,6 +102,7 @@ struct external_weapon_state int fp_counter = 0; // cycles through the model's firing points, for "chain external model fps" weapons float rotate_rate = 0.0f; // current spin rate of the model's Gun_rotation submodels (primaries only) float rotate_ang = 0.0f; // current spin angle of the model's Gun_rotation submodels (primaries only) + bool spin_up_requested = false; // set each frame the bank tries to fire; consumed by update_external_weapon_spin() }; class ship_weapon { From 0879df8aa5d93728cd9411542fd2850d8704257d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Jul 2026 15:07:20 -0400 Subject: [PATCH 3/5] make external_model_num mean what it says; gate firing offsets on drawn models Since the original 2005 implementation, external_model_num was bashed to model_num at parse time when no $External Model File: was given, so every POF-rendered weapon appeared to have an external model. All the checks downstream therefore tested 'has any model' while reading as 'has an external model', and two paths leaked the fallback into gameplay: - ship_fire_secondary applied the external-model firing point offset without checking $Show Secondary Models: (unlike the primary path), so any missile whose own in-flight POF contained gun points would have its launch position offset on every ship in the game. - The AI line-of-sight check, copied from ship_fire_secondary, applied the offset with no draw check for either bank type, so for primaries it could disagree with the actual firing path. external_model_num now stays -1 unless $External Model File: was given. Rendering code (ship_render_weapon_models, the HUD hardpoints gauge, and the gatling instance helper) falls back to the weapon's own model at the point of use, preserving the existing display of missiles on hardpoints. Firing point offsets now require a dedicated external model *and* the bank's models to actually be drawn, in all three paths. In practice this does not change firing behavior, because GPNT chunks are never added to weapon models except when explicitly built as external models. Co-Authored-By: Claude Fable 5 --- code/ai/aicode.cpp | 10 +++++++--- code/hud/hudtarget.cpp | 23 +++++++++++++++-------- code/ship/ship.cpp | 39 ++++++++++++++++++++++++--------------- code/ship/ship.h | 2 +- code/weapon/weapon.h | 2 +- code/weapon/weapons.cpp | 4 ---- 6 files changed, 48 insertions(+), 32 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 728752675c5..16c64c65f9c 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -6775,14 +6775,18 @@ bool check_los(int objnum, int target_objnum, float threshold, int primary_bank, bool is_primary = secondary_bank == -1; ship *shipp = &Ships[firing_ship->instance]; ship_weapon *swp = &shipp->weapons; + ship_info *sip = &Ship_info[shipp->ship_info_index]; weapon_info *wip = &Weapon_info[is_primary ? swp->primary_bank_weapons[primary_bank] : swp->secondary_bank_weapons[secondary_bank]]; - polymodel* pm = model_get(Ship_info[shipp->ship_info_index].model_num); - + polymodel* pm = model_get(sip->model_num); + vec3d pnt = is_primary ? pm->gun_banks[primary_bank].pnt[swp->primary_next_slot[primary_bank]] : pm->missile_banks[secondary_bank].pnt[swp->secondary_next_slot[secondary_bank]]; vec3d firing_point; + // external model firing points only apply when the external models are actually drawn + // (matching ship_fire_primary and ship_fire_secondary) + bool draw_models = is_primary ? sip->draw_primary_models[primary_bank] : sip->draw_secondary_models[secondary_bank]; polymodel* weapon_model = nullptr; - if (wip->external_model_num >= 0) { + if (draw_models && wip->external_model_num >= 0) { weapon_model = model_get(wip->external_model_num); } diff --git a/code/hud/hudtarget.cpp b/code/hud/hudtarget.cpp index 08a3d985a97..01ab8b3fe40 100644 --- a/code/hud/hudtarget.cpp +++ b/code/hud/hudtarget.cpp @@ -7905,11 +7905,13 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) auto ship_pm = model_get(sip->model_num); for (i = 0; i < swp->num_secondary_banks; i++) { - if (swp->secondary_bank_weapons[i] < 0) + if (swp->secondary_bank_weapons[i] < 0 || !sip->draw_secondary_models[i]) continue; auto wip = &Weapon_info[swp->secondary_bank_weapons[i]]; - if (wip->external_model_num < 0 || !sip->draw_secondary_models[i]) + // if the weapon has no dedicated external model, display the weapon's own model, if it has one + int display_model_num = (wip->external_model_num >= 0) ? wip->external_model_num : wip->model_num; + if (display_model_num < 0) continue; auto bank = &ship_pm->missile_banks[i]; @@ -7932,10 +7934,10 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) matrix model_orient; vm_matrix_x_matrix(&model_orient, &object_orient, &slot_orient); - model_render_immediate(&weapon_render_info, wip->external_model_num, &model_orient, &world_position); + model_render_immediate(&weapon_render_info, display_model_num, &model_orient, &world_position); } } else { - auto weapon_pm = model_get(wip->external_model_num); + auto weapon_pm = model_get(display_model_num); num_secondaries_rendered = 0; for(k = 0; k < bank->num_slots; k++) @@ -7971,7 +7973,7 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) matrix model_orient; vm_matrix_x_matrix(&model_orient, &object_orient, &slot_orient); - model_render_immediate(&weapon_render_info, wip->external_model_num, &model_orient, &world_position); + model_render_immediate(&weapon_render_info, display_model_num, &model_orient, &world_position); } } } @@ -7989,7 +7991,12 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) auto bank = &ship_pm->gun_banks[i]; auto wip = (swp->primary_bank_weapons[i] >= 0) ? &Weapon_info[swp->primary_bank_weapons[i]] : nullptr; - if ( wip == nullptr || wip->external_model_num < 0 || !sip->draw_primary_models[i] ) { + // if the weapon has no dedicated external model, display the weapon's own model, if it has one + int display_model_num = -1; + if ( wip != nullptr && sip->draw_primary_models[i] ) + display_model_num = (wip->external_model_num >= 0) ? wip->external_model_num : wip->model_num; + + if ( display_model_num < 0 ) { // no model to draw, so just mark each firing point with a circle for ( k = 0; k < bank->num_slots; k++ ) { vm_vec_unrotate(&subobj_pos, &bank->pnt[k], &object_orient); @@ -8001,7 +8008,7 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) renderCircle((int)draw_point.screen.xyw.x + position[0], (int)draw_point.screen.xyw.y + position[1], 10); } } else { - int external_model_instance = ship_get_external_weapon_model_instance(swp, i); + int external_model_instance = ship_get_external_weapon_model_instance(swp, i, display_model_num); for ( k = 0; k < bank->num_slots; k++ ) { model_render_params weapon_render_info; @@ -8020,7 +8027,7 @@ void HudGaugeHardpoints::render(float /*frametime*/, bool config) matrix model_orient; vm_matrix_x_matrix(&model_orient, &object_orient, &slot_orient); - model_render_immediate(&weapon_render_info, wip->external_model_num, external_model_instance, &model_orient, &world_position); + model_render_immediate(&weapon_render_info, display_model_num, external_model_instance, &model_orient, &world_position); } } } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index e6f95bb4bbc..fe517ac60ea 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -13428,8 +13428,9 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) firepoint_list = nullptr; } + // external model firing points only apply when the external models are actually drawn polymodel *weapon_model = nullptr; - if (sip->draw_primary_models[bank_to_fire] && (winfo_p->external_model_num >= 0)) + if (sip->draw_primary_models[bank_to_fire] && (winfo_p->external_model_num >= 0)) weapon_model = model_get(winfo_p->external_model_num); for (int pt_count = 0; pt_count < point_count; pt_count++) { @@ -14336,8 +14337,10 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) vec3d dir; dir = pm->missile_banks[bank].norm[pnt_index++]; + // external model firing points only apply when the external models are actually drawn + // (matching the primary bank behavior in ship_fire_primary) polymodel *weapon_model = nullptr; - if(wip->external_model_num >= 0){ + if(sip->draw_secondary_models[bank] && wip->external_model_num >= 0){ weapon_model = model_get(wip->external_model_num); } @@ -21503,8 +21506,10 @@ void ship_render_batch_thrusters(object *obj) // the last call (weapons can be swapped mid-mission by SEXPs, scripts, or rearming). The ideal // place to create the instance would be in parse_object_create_sub, but the player can alter // the ship loadout after that function runs. +// display_model_num is the model the caller renders for this bank (the weapon's external model, +// or its own model as the fallback). // Returns the model instance number, or -1 if the bank's weapon doesn't need an instance. -int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank) +int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank, int display_model_num) { int weapon_idx = swp->primary_bank_weapons[bank]; auto &ext = swp->primary_bank_external_weapon[bank]; @@ -21518,16 +21523,16 @@ int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank) ext.model_instance = -1; } - if (weapon_idx >= 0 && Weapon_info[weapon_idx].external_model_num >= 0) + if (weapon_idx >= 0 && display_model_num >= 0) { - auto pm = model_get(Weapon_info[weapon_idx].external_model_num); + auto pm = model_get(display_model_num); // create a model instance only if at least one submodel has gun rotation for (int mn = 0; mn < pm->n_models; mn++) { if (pm->submodel[mn].flags[Model::Submodel_flags::Gun_rotation]) { - ext.model_instance = model_create_instance(model_objnum_special::OBJNUM_NONE, Weapon_info[weapon_idx].external_model_num); + ext.model_instance = model_create_instance(model_objnum_special::OBJNUM_NONE, display_model_num); break; } } @@ -21580,16 +21585,18 @@ void ship_render_weapon_models(model_render_params *ship_render_info, model_draw //primary weapons for ( i = 0; i < swp->num_primary_banks; i++ ) { - if ( swp->primary_bank_weapons[i] < 0 ) { + if ( swp->primary_bank_weapons[i] < 0 || !sip->draw_primary_models[i] ) { continue; } auto wip = &Weapon_info[swp->primary_bank_weapons[i]]; - if ( wip->external_model_num < 0 || !sip->draw_primary_models[i] ) { + // if the weapon has no dedicated external model, display the weapon's own model, if it has one + int display_model_num = (wip->external_model_num >= 0) ? wip->external_model_num : wip->model_num; + if ( display_model_num < 0 ) { continue; } - int external_model_instance = ship_get_external_weapon_model_instance(swp, i); + int external_model_instance = ship_get_external_weapon_model_instance(swp, i, display_model_num); auto bank = &ship_pm->gun_banks[i]; @@ -21615,18 +21622,20 @@ void ship_render_weapon_models(model_render_params *ship_render_info, model_draw matrix slot_orient; ship_get_weapon_model_slot_transform(bank, k, 0.0f, &slot_pnt, &slot_orient); - model_render_queue(ship_render_info, scene, wip->external_model_num, external_model_instance, &slot_orient, &slot_pnt); + model_render_queue(ship_render_info, scene, display_model_num, external_model_instance, &slot_orient, &slot_pnt); } } //secondary weapons for (i = 0; i < swp->num_secondary_banks; i++) { - if ( swp->secondary_bank_weapons[i] < 0 ) { + if ( swp->secondary_bank_weapons[i] < 0 || !sip->draw_secondary_models[i] ) { continue; } auto wip = &Weapon_info[swp->secondary_bank_weapons[i]]; - if ( wip->external_model_num < 0 || !sip->draw_secondary_models[i] ) { + // if the weapon has no dedicated external model, display the weapon's own model, if it has one + int display_model_num = (wip->external_model_num >= 0) ? wip->external_model_num : wip->model_num; + if ( display_model_num < 0 ) { continue; } @@ -21638,10 +21647,10 @@ void ship_render_weapon_models(model_render_params *ship_render_info, model_draw matrix slot_orient; ship_get_weapon_model_slot_transform(bank, k, 0.0f, &slot_pnt, &slot_orient); - model_render_queue(ship_render_info, scene, wip->external_model_num, &slot_orient, &slot_pnt); + model_render_queue(ship_render_info, scene, display_model_num, &slot_orient, &slot_pnt); } } else { - auto weapon_pm = model_get(wip->external_model_num); + auto weapon_pm = model_get(display_model_num); int num_secondaries_rendered = 0; for ( k = 0; k < bank->num_slots; k++ ) { @@ -21660,7 +21669,7 @@ void ship_render_weapon_models(model_render_params *ship_render_info, model_draw matrix slot_orient; ship_get_weapon_model_slot_transform(bank, k, (1.0f - reload_pct) * weapon_pm->rad, &slot_pnt, &slot_orient); - model_render_queue(ship_render_info, scene, wip->external_model_num, &slot_orient, &slot_pnt); + model_render_queue(ship_render_info, scene, display_model_num, &slot_orient, &slot_pnt); } } } diff --git a/code/ship/ship.h b/code/ship/ship.h index 87aebe33bd6..899463f098c 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1824,7 +1824,7 @@ extern int ship_stop_fire_primary(object * obj); extern int ship_fire_primary(object * objp, int force = 0, bool rollback_shot = false); extern vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, bool advance_counter, int sub_shot = 0); extern void ship_get_weapon_model_slot_transform(const w_bank *bank, int slot, float reload_slide_back, vec3d *outpnt, matrix *outorient); -extern int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank); +extern int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank, int display_model_num); extern int ship_fire_secondary(object * objp, int allow_swarm = 0, bool rollback_shot = false ); bool ship_start_secondary_fire(object* objp); bool ship_stop_secondary_fire(object* objp); diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index d8d7fa07250..4b1ce288150 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -386,7 +386,7 @@ struct weapon_info char pofbitmap_name[MAX_FILENAME_LEN]; // Name of the pof representing this if POF, or bitmap filename if bitmap int model_num; // modelnum of weapon -- -1 if no model char external_model_name[MAX_FILENAME_LEN]; //the model rendered on the weapon points of a ship - int external_model_num; //the model rendered on the weapon points of a ship + int external_model_num; // modelnum of the model rendered on the weapon points of a ship; -1 if $External Model File: was not given std::unique_ptr tech_desc; // weapon's description (in tech database) char tech_anim_filename[MAX_FILENAME_LEN]; // weapon's tech room animation diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 7e742a42369..a153211ddc6 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -8844,10 +8844,6 @@ static void weapon_page_in_one(weapon_info *wip, bool load_graphics) if (VALID_FNAME(wip->external_model_name)) wip->external_model_num = model_load(wip->external_model_name); - if (wip->external_model_num == -1) - wip->external_model_num = wip->model_num; - - //Load shockwaves shockwave_create_info_load(&wip->shockwave); shockwave_create_info_load(&wip->dinky_shockwave); From 482559808d0f70501c328fa4ffa9854cba453a2b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Jul 2026 15:08:17 -0400 Subject: [PATCH 4/5] warn on external weapon model conventions that are silently ignored Two load-bearing conventions of the external weapon model feature had no diagnostics: - Only the first gun bank of an external model supplies firing points; additional banks are silently ignored. Warn when an external model has more than one gun bank. - $Show Primary Models: / $Show Secondary Models: must list exactly one bool per weapon bank; missing entries silently default to not drawing. Warn when the list length doesn't match the bank count. Co-Authored-By: Claude Fable 5 --- code/ship/ship.cpp | 8 ++++++-- code/weapon/weapons.cpp | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index fe517ac60ea..df67ad57a2e 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -3699,7 +3699,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool if(optional_string("$Show Primary Models:")) { sip->flags.set(Ship::Info_Flags::Draw_weapon_models); - stuff_bool_list(sip->draw_primary_models, sip->num_primary_banks); + size_t num_bools = stuff_bool_list(sip->draw_primary_models, sip->num_primary_banks); + if (num_bools != static_cast(sip->num_primary_banks)) + error_display(0, "$Show Primary Models: on ship class %s lists " SIZE_T_ARG " entries, but the ship has %d primary banks.", sip->name, num_bools, sip->num_primary_banks); } // Set the weapons filter used in weapons loadout (for secondary weapons) @@ -3712,7 +3714,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool if(optional_string("$Show Secondary Models:")) { sip->flags.set(Ship::Info_Flags::Draw_weapon_models); - stuff_bool_list(sip->draw_secondary_models, sip->num_secondary_banks); + size_t num_bools = stuff_bool_list(sip->draw_secondary_models, sip->num_secondary_banks); + if (num_bools != static_cast(sip->num_secondary_banks)) + error_display(0, "$Show Secondary Models: on ship class %s lists " SIZE_T_ARG " entries, but the ship has %d secondary banks.", sip->name, num_bools, sip->num_secondary_banks); } if (optional_string("$Ship Recoil Modifier:")){ diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index a153211ddc6..53a4889de7d 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -8844,6 +8844,15 @@ static void weapon_page_in_one(weapon_info *wip, bool load_graphics) if (VALID_FNAME(wip->external_model_name)) wip->external_model_num = model_load(wip->external_model_name); + if (wip->external_model_num >= 0) + { + polymodel *external_pm = model_get(wip->external_model_num); + + // only the first gun bank of an external model supplies firing points + if (external_pm->n_guns > 1) + Warning(LOCATION, "External model %s of weapon %s has %d gun banks; only the firing points of the first bank are used.", wip->external_model_name, wip->name, external_pm->n_guns); + } + //Load shockwaves shockwave_create_info_load(&wip->shockwave); shockwave_create_info_load(&wip->dinky_shockwave); From 119ff479257a84b6ececc43cc33c1c2849985d8b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 Jul 2026 15:30:50 -0400 Subject: [PATCH 5/5] orient external weapon models along their slot's firing normal External weapon models were always rendered pointing ship-forward, and reloading missiles slid backward along fixed -Z, regardless of the gun or missile point's normal -- so a bank angled 45 degrees outward fired along its normal while its mounted model pointed dead ahead. The per-slot angle offset was also applied as a roll about ship-forward rather than about the firing axis. ship_get_weapon_model_slot_transform now builds the slot orientation from the firing normal (identity for the usual dead-ahead normal, so existing forward-facing mounts render exactly as before) with the angle-offset roll applied about that axis, and slides reloading missiles along the normal. Correspondingly, ship_get_external_model_fp_offset now rotates the external model's firing-point offset by the same slot transform, so shots continue to emerge from the drawn barrels on angled mounts, and now also respect the angle-offset roll (previously the offset was added unrotated, mismatching rolled models). Co-Authored-By: Claude Fable 5 --- code/ai/aicode.cpp | 4 +++- code/ship/ship.cpp | 58 ++++++++++++++++++++++++++++++++-------------- code/ship/ship.h | 2 +- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 16c64c65f9c..9cb03ea009a 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -6792,7 +6792,9 @@ bool check_los(int objnum, int target_objnum, float threshold, int primary_bank, // use the same firing point the next shot will use auto ext = is_primary ? &swp->primary_bank_external_weapon[primary_bank] : &swp->secondary_bank_external_weapon[secondary_bank]; - vec3d external_fp_offset = ship_get_external_model_fp_offset(ext, wip, weapon_model, false); + auto ship_bank = is_primary ? &pm->gun_banks[primary_bank] : &pm->missile_banks[secondary_bank]; + int slot = is_primary ? swp->primary_next_slot[primary_bank] : swp->secondary_next_slot[secondary_bank]; + vec3d external_fp_offset = ship_get_external_model_fp_offset(ext, wip, weapon_model, ship_bank, slot, false); vm_vec_add2(&pnt, &external_fp_offset); vm_vec_unrotate(&firing_point, &pnt, &firing_ship->orient); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index df67ad57a2e..2ac589a6046 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -12773,19 +12773,23 @@ bool in_autoaim_fov(ship *shipp, int bank_to_fire, object *obj) } -// Gets the offset, relative to the ship's firing point, of the firing point within an external -// weapon model that the next shot should be fired from. Handles the "chain external model fps" -// weapon flag: chained weapons cycle through the external model's firing points using the bank's -// chain counter; non-chained weapons use the sub_shot index directly. +// Gets the offset, in the ship model's frame, from the ship's firing point to the firing point +// within an external weapon model that the next shot should be fired from. Handles the "chain +// external model fps" weapon flag: chained weapons cycle through the external model's firing +// points using the bank's chain counter; non-chained weapons use the sub_shot index directly. +// The offset is rotated to match how the model is rendered on the slot (aligned to the slot's +// firing normal and rolled by its angle offset; see ship_get_weapon_model_slot_transform). // -// ext is the bank's external weapon state. advance_counter should be true when actually firing -// a shot, and false for queries such as the AI's line-of-sight check. -vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, bool advance_counter, int sub_shot) +// ext is the bank's external weapon state; ship_bank and slot identify the ship firing point +// the weapon model is mounted on. advance_counter should be true when actually firing a shot, +// and false for queries such as the AI's line-of-sight check. +vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, const w_bank *ship_bank, int slot, bool advance_counter, int sub_shot) { if (weapon_model == nullptr || weapon_model->n_guns < 1 || weapon_model->gun_banks[0].num_slots < 1) return vmd_zero_vector; auto &bank = weapon_model->gun_banks[0]; + vec3d local_offset; if (wip->wi_flags[Weapon::Info_Flags::External_weapon_fp]) { @@ -12797,11 +12801,20 @@ vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon if (advance_counter) ext->fp_counter++; - return bank.pnt[fp]; + local_offset = bank.pnt[fp]; + } + else + { + Assertion(sub_shot >= 0 && sub_shot < bank.num_slots, "sub_shot %d is out of range for the external model of weapon %s, which has %d firing points", sub_shot, wip->name, bank.num_slots); + local_offset = bank.pnt[sub_shot]; } - Assertion(sub_shot >= 0 && sub_shot < bank.num_slots, "sub_shot %d is out of range for the external model of weapon %s, which has %d firing points", sub_shot, wip->name, bank.num_slots); - return bank.pnt[sub_shot]; + vec3d slot_pnt, offset; + matrix slot_orient; + ship_get_weapon_model_slot_transform(ship_bank, slot, 0.0f, &slot_pnt, &slot_orient); + vm_vec_unrotate(&offset, &local_offset, &slot_orient); + + return offset; } // fires a primary weapon for the given object. It also handles multiplayer cases. @@ -13485,7 +13498,7 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) vec3d dir; dir = pm->gun_banks[bank_to_fire].norm[pt]; - vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->primary_bank_external_weapon[bank_to_fire], winfo_p, weapon_model, true, s); + vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->primary_bank_external_weapon[bank_to_fire], winfo_p, weapon_model, &pm->gun_banks[bank_to_fire], pt, true, s); vm_vec_add2(&pnt, &external_fp_offset); vm_vec_unrotate(&gun_point, &pnt, &obj->orient); @@ -14339,7 +14352,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) shipp->secondary_point_reload_pct.set(bank, pnt_index, 0.0f); pnt = pm->missile_banks[bank].pnt[pnt_index]; vec3d dir; - dir = pm->missile_banks[bank].norm[pnt_index++]; + dir = pm->missile_banks[bank].norm[pnt_index]; // external model firing points only apply when the external models are actually drawn // (matching the primary bank behavior in ship_fire_primary) @@ -14349,8 +14362,9 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) } // chained weapons cycle through the external model's firing points; other weapons use the 0 index slot - vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->secondary_bank_external_weapon[bank], wip, weapon_model, true); + vec3d external_fp_offset = ship_get_external_model_fp_offset(&swp->secondary_bank_external_weapon[bank], wip, weapon_model, &pm->missile_banks[bank], pnt_index, true); vm_vec_add2(&pnt, &external_fp_offset); + pnt_index++; vm_vec_unrotate(&missile_point, &pnt, &obj->orient); vm_vec_add(&firing_pos, &missile_point, &obj->pos); @@ -21549,19 +21563,27 @@ int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank, int disp } // Computes the position and orientation, in the ship model's frame, at which to render an -// external weapon model on slot `slot` of weapon bank `bank`. reload_slide_back is the -// distance a partially reloaded missile is slid backward along the bank's -Z axis; pass -// 0.0f for primaries and launcher-style secondaries, which don't slide. +// external weapon model on slot `slot` of weapon bank `bank`. The model points along the +// slot's firing normal and is "banked" (rolled) by the slot's angle offset. +// reload_slide_back is the distance a partially reloaded missile is slid backward along the +// firing normal; pass 0.0f for primaries and launcher-style secondaries, which don't slide. void ship_get_weapon_model_slot_transform(const w_bank *bank, int slot, float reload_slide_back, vec3d *outpnt, matrix *outorient) { + // point the model along the slot's firing direction (identity for the usual dead-ahead normal) + matrix norm_orient; + vm_vector_2_matrix_norm(&norm_orient, &bank->norm[slot]); + // "Bank" the external model by the angle offset angles angs = { 0.0f, bank->external_model_angle_offset[slot], 0.0f }; - vm_angles_2_matrix(outorient, &angs); + matrix bank_orient; + vm_angles_2_matrix(&bank_orient, &angs); + + vm_matrix_x_matrix(outorient, &norm_orient, &bank_orient); *outpnt = bank->pnt[slot]; if (reload_slide_back > 0.0f) - vm_vec_scale_add2(outpnt, &vmd_z_vector, -reload_slide_back); + vm_vec_scale_add2(outpnt, &bank->norm[slot], -reload_slide_back); } void ship_render_weapon_models(model_render_params *ship_render_info, model_draw_list *scene, object *obj) diff --git a/code/ship/ship.h b/code/ship/ship.h index 899463f098c..034978575c0 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1822,7 +1822,7 @@ extern void ship_actually_depart(int shipnum, int method = SHIP_DEPARTED_WARP); 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 vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, bool advance_counter, int sub_shot = 0); +extern vec3d ship_get_external_model_fp_offset(external_weapon_state *ext, const weapon_info *wip, const polymodel *weapon_model, const w_bank *ship_bank, int slot, bool advance_counter, int sub_shot = 0); extern void ship_get_weapon_model_slot_transform(const w_bank *bank, int slot, float reload_slide_back, vec3d *outpnt, matrix *outorient); extern int ship_get_external_weapon_model_instance(ship_weapon *swp, int bank, int display_model_num); extern int ship_fire_secondary(object * objp, int allow_swarm = 0, bool rollback_shot = false );