From 265f374e1c01a28d3507d8045ebb1ee658cfb05a Mon Sep 17 00:00:00 2001 From: Dustinspace217 Date: Sun, 11 Oct 2026 22:51:27 -0700 Subject: [PATCH 1/4] Add weapon_info::clone Copies every member of a weapon class, following ship_info::clone, so a weapon can be created from another as ship templates do. The CLONE_UNIQUE_PTR macro moves from ship.cpp to utils/string_utils.h so both clone functions share it. --- code/ship/ship.cpp | 2 - code/utils/string_utils.h | 2 + code/weapon/weapon.h | 1 + code/weapon/weapons.cpp | 260 ++++++++++++++++++++++++++++++++++++ test/src/weapon/weapons.cpp | 37 +++++ 5 files changed, 300 insertions(+), 2 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 123ba8783b4..f1d45b05e94 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1083,8 +1083,6 @@ const float DEFAULT_ASK_HELP_HULL_PERCENT = 0.3f; // percent hull at which shi const float AWACS_HELP_HULL_HI = 0.75f; // percent hull at which ship will ask for help const float AWACS_HELP_HULL_LOW = 0.25f; // percent hull at which ship will ask for help -#define CLONE_UNIQUE_PTR(attribute) attribute = util::unique_copy(other.attribute.get(), true) - void ship_info::clone(const ship_info& other) { strcpy_s(name, other.name); diff --git a/code/utils/string_utils.h b/code/utils/string_utils.h index 707e9b80523..59949999f7f 100644 --- a/code/utils/string_utils.h +++ b/code/utils/string_utils.h @@ -39,3 +39,5 @@ std::unique_ptr unique_copy(const char *str, bool null_if_empty); SCP_vm_unique_ptr vm_unique_copy(const char *str, bool null_if_empty); } // namespace util + +#define CLONE_UNIQUE_PTR(attribute) attribute = util::unique_copy(other.attribute.get(), true) diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index ecb122486fd..1df50bf5f46 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -961,6 +961,7 @@ struct weapon_info bool has_display_name() const; void reset(); + void clone(const weapon_info& other); }; extern special_flag_def_list_new&> Weapon_Info_Flags[]; diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index f147f081c8a..3c3ab5247be 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -66,6 +66,7 @@ #include "particle/volumes/PointVolume.h" #include "tracing/Monitor.h" #include "tracing/tracing.h" +#include "utils/string_utils.h" #include "weapon.h" #include "model/modelrender.h" @@ -10158,6 +10159,265 @@ void weapon_info::reset() this->on_create_program = actions::ProgramSet(); } +void weapon_info::clone(const weapon_info& other) +{ + strcpy_s(name, other.name); + display_name = other.display_name; + strcpy_s(title, other.title); + CLONE_UNIQUE_PTR(desc); + strcpy_s(altSubsysName, other.altSubsysName); + strcpy_s(pofbitmap_name, other.pofbitmap_name); + model_num = other.model_num; + strcpy_s(external_model_name, other.external_model_name); + external_model_num = other.external_model_num; + CLONE_UNIQUE_PTR(tech_desc); + strcpy_s(tech_anim_filename, other.tech_anim_filename); + strcpy_s(tech_title, other.tech_title); + strcpy_s(tech_model, other.tech_model); + hud_target_lod = other.hud_target_lod; + num_detail_levels = other.num_detail_levels; + memcpy(detail_distance, other.detail_distance, sizeof(detail_distance)); + subtype = other.subtype; + render_type = other.render_type; + closeup_pos = other.closeup_pos; + closeup_zoom = other.closeup_zoom; + icon_closeup_pos = other.icon_closeup_pos; + icon_closeup_zoom = other.icon_closeup_zoom; + strcpy_s(hud_filename, other.hud_filename); + hud_image_index = other.hud_image_index; + laser_bitmap = other.laser_bitmap; + laser_headon_bitmap = other.laser_headon_bitmap; + laser_glow_bitmap = other.laser_glow_bitmap; + laser_glow_headon_bitmap = other.laser_glow_headon_bitmap; + laser_headon_switch_rate = other.laser_headon_switch_rate; + laser_headon_switch_ang = other.laser_headon_switch_ang; + laser_length = other.laser_length; + laser_length_by_frametime = other.laser_length_by_frametime; + bitmap_color = other.bitmap_color; + laser_color_1 = other.laser_color_1; + laser_color_2 = other.laser_color_2; + laser_head_radius = other.laser_head_radius; + laser_tail_radius = other.laser_tail_radius; + laser_glow_length_scale = other.laser_glow_length_scale; + laser_glow_head_scale = other.laser_glow_head_scale; + laser_glow_tail_scale = other.laser_glow_tail_scale; + laser_min_pixel_size = other.laser_min_pixel_size; + laser_pos_offset = other.laser_pos_offset; + collision_radius_override = other.collision_radius_override; + light_color_set = other.light_color_set; + light_color = other.light_color; + light_radius = other.light_radius; + max_speed = other.max_speed; + acceleration_time = other.acceleration_time; + vel_inherit_amount = other.vel_inherit_amount; + free_flight_time = other.free_flight_time; + free_flight_speed_factor = other.free_flight_speed_factor; + gravity_const = other.gravity_const; + mass = other.mass; + fire_wait = other.fire_wait; + max_delay = other.max_delay; + min_delay = other.min_delay; + damage = other.damage; + damage_time = other.damage_time; + atten_damage = other.atten_damage; + damage_incidence_max = other.damage_incidence_max; + damage_incidence_min = other.damage_incidence_min; + shockwave = other.shockwave; + dinky_shockwave = other.dinky_shockwave; + arm_time = other.arm_time; + arm_dist = other.arm_dist; + arm_radius = other.arm_radius; + det_range = other.det_range; + det_radius = other.det_radius; + proximity_radius = other.proximity_radius; + proximity_iff = other.proximity_iff; + proximity_species = other.proximity_species; + proximity_type = other.proximity_type; + proximity_class = other.proximity_class; + proximity_relation_mask = other.proximity_relation_mask; + proximity_detonate_chance = other.proximity_detonate_chance; + proximity_stealth_multiplier = other.proximity_stealth_multiplier; + mine_sensors_range = other.mine_sensors_range; + mine_targetable_range = other.mine_targetable_range; + mine_chase_duration = other.mine_chase_duration; + mine_detonates_on_chase_timeout = other.mine_detonates_on_chase_timeout; + mine_chase_cooldown = other.mine_chase_cooldown; + flak_detonation_accuracy = other.flak_detonation_accuracy; + flak_targeting_accuracy = other.flak_targeting_accuracy; + untargeted_flak_range_penalty = other.untargeted_flak_range_penalty; + armor_factor = other.armor_factor; + shield_factor = other.shield_factor; + subsystem_factor = other.subsystem_factor; + life_min = other.life_min; + life_max = other.life_max; + lifetime = other.lifetime; + energy_consumed = other.energy_consumed; + wi_flags = other.wi_flags; + turn_time = other.turn_time; + turn_accel_time = other.turn_accel_time; + cargo_size = other.cargo_size; + autoaim_fov = other.autoaim_fov; + rearm_rate = other.rearm_rate; + disallow_rearm = other.disallow_rearm; + reloaded_per_batch = other.reloaded_per_batch; + weapon_range = other.weapon_range; + optimum_range = other.optimum_range; + weapon_min_range = other.weapon_min_range; + aiming_flags = other.aiming_flags; + minimum_convergence_distance = other.minimum_convergence_distance; + convergence_distance = other.convergence_distance; + pierce_objects = other.pierce_objects; + spawn_children_on_pierce = other.spawn_children_on_pierce; + num_spawn_weapons_defined = other.num_spawn_weapons_defined; + maximum_children_spawned = other.maximum_children_spawned; + memcpy(spawn_info, other.spawn_info, sizeof(spawn_info)); + lifetime_variation_factor_when_child = other.lifetime_variation_factor_when_child; + swarm_count = other.swarm_count; + SwarmWait = other.SwarmWait; + target_restrict = other.target_restrict; + target_restrict_objecttypes = other.target_restrict_objecttypes; + multi_lock = other.multi_lock; + max_seeking = other.max_seeking; + max_seekers_per_target = other.max_seekers_per_target; + ship_restrict = other.ship_restrict; + ship_restrict_strings = other.ship_restrict_strings; + trigger_lock = other.trigger_lock; + launch_reset_locks = other.launch_reset_locks; + auto_target_method = other.auto_target_method; + acquire_method = other.acquire_method; + min_lock_time = other.min_lock_time; + lock_pixels_per_sec = other.lock_pixels_per_sec; + catchup_pixels_per_sec = other.catchup_pixels_per_sec; + catchup_pixel_penalty = other.catchup_pixel_penalty; + lock_fov = other.lock_fov; + fov = other.fov; + seeker_strength = other.seeker_strength; + pre_launch_snd = other.pre_launch_snd; + pre_launch_snd_min_interval = other.pre_launch_snd_min_interval; + launch_snd = other.launch_snd; + cockpit_launch_snd = other.cockpit_launch_snd; + impact_snd = other.impact_snd; + disarmed_impact_snd = other.disarmed_impact_snd; + shield_impact_snd = other.shield_impact_snd; + flyby_snd = other.flyby_snd; + ambient_snd = other.ambient_snd; + start_firing_snd = other.start_firing_snd; + loop_firing_snd = other.loop_firing_snd; + linked_loop_firing_snd = other.linked_loop_firing_snd; + end_firing_snd = other.end_firing_snd; + hud_tracking_snd = other.hud_tracking_snd; + hud_locked_snd = other.hud_locked_snd; + hud_in_flight_snd = other.hud_in_flight_snd; + in_flight_play_type = other.in_flight_play_type; + tr_info = other.tr_info; + strcpy_s(icon_filename, other.icon_filename); + strcpy_s(anim_filename, other.anim_filename); + selection_effect = other.selection_effect; + fs2_effect_grid_color = other.fs2_effect_grid_color; + fs2_effect_scanline_color = other.fs2_effect_scanline_color; + fs2_effect_grid_density = other.fs2_effect_grid_density; + fs2_effect_wireframe_color = other.fs2_effect_wireframe_color; + shield_impact_effect_radius = other.shield_impact_effect_radius; + shield_impact_explosion_radius = other.shield_impact_explosion_radius; + impact_weapon_expl_effect = other.impact_weapon_expl_effect; + dinky_impact_weapon_expl_effect = other.dinky_impact_weapon_expl_effect; + flash_impact_weapon_expl_effect = other.flash_impact_weapon_expl_effect; + piercing_impact_effect = other.piercing_impact_effect; + piercing_impact_secondary_effect = other.piercing_impact_secondary_effect; + conditional_impacts = other.conditional_impacts; + muzzle_effect = other.muzzle_effect; + state_effects = other.state_effects; + emp_intensity = other.emp_intensity; + emp_time = other.emp_time; + recoil_modifier = other.recoil_modifier; + shudder_modifier = other.shudder_modifier; + weapon_reduce = other.weapon_reduce; + afterburner_reduce = other.afterburner_reduce; + vamp_regen = other.vamp_regen; + tag_time = other.tag_time; + tag_level = other.tag_level; + field_of_fire = other.field_of_fire; + fof_spread_rate = other.fof_spread_rate; + fof_reset_rate = other.fof_reset_rate; + max_fof_spread = other.max_fof_spread; + firing_pattern = other.firing_pattern; + shots = other.shots; + cycle_multishot = other.cycle_multishot; + cs_num_fired = other.cs_num_fired; + cs_radius = other.cs_radius; + cs_twist = other.cs_twist; + cs_crotate = other.cs_crotate; + cs_delay = other.cs_delay; + cs_random_angle = other.cs_random_angle; + cs_angle = other.cs_angle; + elec_time = other.elec_time; + elec_eng_mult = other.elec_eng_mult; + elec_weap_mult = other.elec_weap_mult; + elec_beam_mult = other.elec_beam_mult; + elec_sensors_mult = other.elec_sensors_mult; + elec_randomness = other.elec_randomness; + elec_use_new_style = other.elec_use_new_style; + SSM_index = other.SSM_index; + lssm_warpout_delay = other.lssm_warpout_delay; + lssm_warpin_delay = other.lssm_warpin_delay; + lssm_stage5_vel = other.lssm_stage5_vel; + lssm_warpin_radius = other.lssm_warpin_radius; + lssm_lock_range = other.lssm_lock_range; + lssm_warpeffect = other.lssm_warpeffect; + b_info = other.b_info; + particle_spewers = other.particle_spewers; + cm_aspect_effectiveness = other.cm_aspect_effectiveness; + cm_heat_effectiveness = other.cm_heat_effectiveness; + cm_effective_rad = other.cm_effective_rad; + cm_detonation_rad = other.cm_detonation_rad; + cm_launch_vec = other.cm_launch_vec; + cm_launch_speed = other.cm_launch_speed; + cm_launch_variance = other.cm_launch_variance; + cm_kill_single = other.cm_kill_single; + cmeasure_timer_interval = other.cmeasure_timer_interval; + cmeasure_firewait = other.cmeasure_firewait; + cmeasure_use_firewait = other.cmeasure_use_firewait; + cmeasure_failure_delay_multiplier_ai = other.cmeasure_failure_delay_multiplier_ai; + cmeasure_success_delay_multiplier_ai = other.cmeasure_success_delay_multiplier_ai; + weapon_submodel_rotate_accell = other.weapon_submodel_rotate_accell; + weapon_submodel_rotate_vel = other.weapon_submodel_rotate_vel; + damage_type_idx = other.damage_type_idx; + damage_type_idx_sav = other.damage_type_idx_sav; + armor_type_idx = other.armor_type_idx; + alpha_max = other.alpha_max; + alpha_min = other.alpha_min; + alpha_cycle = other.alpha_cycle; + weapon_hitpoints = other.weapon_hitpoints; + burst_shots = other.burst_shots; + burst_delay = other.burst_delay; + burst_flags = other.burst_flags; + thruster_flame = other.thruster_flame; + thruster_glow = other.thruster_glow; + thruster_glow_factor = other.thruster_glow_factor; + target_lead_scaler = other.target_lead_scaler; + num_targeting_priorities = other.num_targeting_priorities; + memcpy(targeting_priorities, other.targeting_priorities, sizeof(targeting_priorities)); + failure_rate = other.failure_rate; + CLONE_UNIQUE_PTR(failure_sub_name); + failure_sub = other.failure_sub; + num_substitution_patterns = other.num_substitution_patterns; + memcpy(weapon_substitution_pattern, other.weapon_substitution_pattern, sizeof(weapon_substitution_pattern)); + memcpy(weapon_substitution_pattern_names, + other.weapon_substitution_pattern_names, + sizeof(weapon_substitution_pattern_names)); + score = other.score; + custom_data = other.custom_data; + custom_strings = other.custom_strings; + impact_decal = other.impact_decal; + on_create_program = other.on_create_program; + animations = other.animations; + weapon_launch_curves = other.weapon_launch_curves; + weapon_curves = other.weapon_curves; + weapon_hit_curves = other.weapon_hit_curves; + beam_curves = other.beam_curves; + beam_hit_curves = other.beam_hit_curves; +} + const char* weapon_info::get_display_name() const { if (has_display_name()) diff --git a/test/src/weapon/weapons.cpp b/test/src/weapon/weapons.cpp index f0722c1a973..d507b065d49 100644 --- a/test/src/weapon/weapons.cpp +++ b/test/src/weapon/weapons.cpp @@ -4,6 +4,7 @@ #include "util/FSTestFixture.h" #include "util/test_util.h" +#include "utils/string_utils.h" #include "weapon/weapon.h" class WeaponsParseTest : public test::FSTestFixture { @@ -34,3 +35,39 @@ TEST_F(WeaponsParseTest, description_too_many_lines) { ASSERT_THROW(weapon_init(), os::dialogs::WarningException); } + +TEST(WeaponInfoCloneTest, copies_members) { + weapon_info src; + strcpy_s(src.name, "Template Laser"); + src.display_name = "Shown Name"; + src.desc = util::unique_copy("a description", true); + src.failure_sub_name = util::unique_copy("Sub Weapon", true); + src.detail_distance[2] = 1234; + strcpy_s(src.weapon_substitution_pattern_names[1], "Other Laser"); + src.spawn_info[1].spawn_count = 7; + src.damage = 42.5f; + src.wi_flags.set(Weapon::Info_Flags::Homing_heat); + src.proximity_iff = {1, 3}; + src.b_info.beam_iff_miss_factor[2] = {{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}}; + src.b_info.t5info.burst_rot_pattern = {0.25f}; + src.custom_data["key"] = "value"; + + weapon_info dest; + dest.clone(src); + + EXPECT_STREQ(dest.name, "Template Laser"); + EXPECT_EQ(dest.display_name, "Shown Name"); + ASSERT_NE(dest.desc, nullptr); + EXPECT_STREQ(dest.desc.get(), "a description"); + ASSERT_NE(dest.failure_sub_name, nullptr); + EXPECT_STREQ(dest.failure_sub_name.get(), "Sub Weapon"); + EXPECT_EQ(dest.detail_distance[2], 1234); + EXPECT_STREQ(dest.weapon_substitution_pattern_names[1], "Other Laser"); + EXPECT_EQ(dest.spawn_info[1].spawn_count, 7); + EXPECT_EQ(dest.damage, 42.5f); + EXPECT_TRUE(dest.wi_flags[Weapon::Info_Flags::Homing_heat]); + EXPECT_EQ(dest.proximity_iff, src.proximity_iff); + EXPECT_EQ(dest.b_info.beam_iff_miss_factor, src.b_info.beam_iff_miss_factor); + EXPECT_EQ(dest.b_info.t5info.burst_rot_pattern, src.b_info.t5info.burst_rot_pattern); + EXPECT_EQ(dest.custom_data, src.custom_data); +} From 076c29822d4916badfa4a873159b43f167f39c6d Mon Sep 17 00:00:00 2001 From: Dustinspace217 Date: Mon, 12 Oct 2026 01:01:28 -0700 Subject: [PATCH 2/4] Group weapon_info::clone fields as the struct declares them --- code/weapon/weapons.cpp | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 3c3ab5247be..a7d094b3648 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -10166,31 +10166,38 @@ void weapon_info::clone(const weapon_info& other) strcpy_s(title, other.title); CLONE_UNIQUE_PTR(desc); strcpy_s(altSubsysName, other.altSubsysName); + strcpy_s(pofbitmap_name, other.pofbitmap_name); model_num = other.model_num; strcpy_s(external_model_name, other.external_model_name); external_model_num = other.external_model_num; + CLONE_UNIQUE_PTR(tech_desc); strcpy_s(tech_anim_filename, other.tech_anim_filename); strcpy_s(tech_title, other.tech_title); strcpy_s(tech_model, other.tech_model); + hud_target_lod = other.hud_target_lod; num_detail_levels = other.num_detail_levels; memcpy(detail_distance, other.detail_distance, sizeof(detail_distance)); subtype = other.subtype; render_type = other.render_type; + closeup_pos = other.closeup_pos; closeup_zoom = other.closeup_zoom; icon_closeup_pos = other.icon_closeup_pos; icon_closeup_zoom = other.icon_closeup_zoom; + strcpy_s(hud_filename, other.hud_filename); hud_image_index = other.hud_image_index; + laser_bitmap = other.laser_bitmap; laser_headon_bitmap = other.laser_headon_bitmap; laser_glow_bitmap = other.laser_glow_bitmap; laser_glow_headon_bitmap = other.laser_glow_headon_bitmap; laser_headon_switch_rate = other.laser_headon_switch_rate; laser_headon_switch_ang = other.laser_headon_switch_ang; + laser_length = other.laser_length; laser_length_by_frametime = other.laser_length_by_frametime; bitmap_color = other.bitmap_color; @@ -10203,10 +10210,13 @@ void weapon_info::clone(const weapon_info& other) laser_glow_tail_scale = other.laser_glow_tail_scale; laser_min_pixel_size = other.laser_min_pixel_size; laser_pos_offset = other.laser_pos_offset; + collision_radius_override = other.collision_radius_override; + light_color_set = other.light_color_set; light_color = other.light_color; light_radius = other.light_radius; + max_speed = other.max_speed; acceleration_time = other.acceleration_time; vel_inherit_amount = other.vel_inherit_amount; @@ -10217,13 +10227,16 @@ void weapon_info::clone(const weapon_info& other) fire_wait = other.fire_wait; max_delay = other.max_delay; min_delay = other.min_delay; + damage = other.damage; damage_time = other.damage_time; atten_damage = other.atten_damage; damage_incidence_max = other.damage_incidence_max; damage_incidence_min = other.damage_incidence_min; + shockwave = other.shockwave; dinky_shockwave = other.dinky_shockwave; + arm_time = other.arm_time; arm_dist = other.arm_dist; arm_radius = other.arm_radius; @@ -10245,14 +10258,19 @@ void weapon_info::clone(const weapon_info& other) flak_detonation_accuracy = other.flak_detonation_accuracy; flak_targeting_accuracy = other.flak_targeting_accuracy; untargeted_flak_range_penalty = other.untargeted_flak_range_penalty; + armor_factor = other.armor_factor; shield_factor = other.shield_factor; subsystem_factor = other.subsystem_factor; + life_min = other.life_min; life_max = other.life_max; lifetime = other.lifetime; + energy_consumed = other.energy_consumed; + wi_flags = other.wi_flags; + turn_time = other.turn_time; turn_accel_time = other.turn_accel_time; cargo_size = other.cargo_size; @@ -10263,35 +10281,48 @@ void weapon_info::clone(const weapon_info& other) weapon_range = other.weapon_range; optimum_range = other.optimum_range; weapon_min_range = other.weapon_min_range; + aiming_flags = other.aiming_flags; minimum_convergence_distance = other.minimum_convergence_distance; convergence_distance = other.convergence_distance; + pierce_objects = other.pierce_objects; spawn_children_on_pierce = other.spawn_children_on_pierce; + num_spawn_weapons_defined = other.num_spawn_weapons_defined; maximum_children_spawned = other.maximum_children_spawned; memcpy(spawn_info, other.spawn_info, sizeof(spawn_info)); + lifetime_variation_factor_when_child = other.lifetime_variation_factor_when_child; + swarm_count = other.swarm_count; SwarmWait = other.SwarmWait; + target_restrict = other.target_restrict; target_restrict_objecttypes = other.target_restrict_objecttypes; multi_lock = other.multi_lock; max_seeking = other.max_seeking; max_seekers_per_target = other.max_seekers_per_target; + ship_restrict = other.ship_restrict; ship_restrict_strings = other.ship_restrict_strings; + trigger_lock = other.trigger_lock; launch_reset_locks = other.launch_reset_locks; + auto_target_method = other.auto_target_method; + acquire_method = other.acquire_method; min_lock_time = other.min_lock_time; lock_pixels_per_sec = other.lock_pixels_per_sec; catchup_pixels_per_sec = other.catchup_pixels_per_sec; catchup_pixel_penalty = other.catchup_pixel_penalty; lock_fov = other.lock_fov; + fov = other.fov; + seeker_strength = other.seeker_strength; + pre_launch_snd = other.pre_launch_snd; pre_launch_snd_min_interval = other.pre_launch_snd_min_interval; launch_snd = other.launch_snd; @@ -10305,11 +10336,14 @@ void weapon_info::clone(const weapon_info& other) loop_firing_snd = other.loop_firing_snd; linked_loop_firing_snd = other.linked_loop_firing_snd; end_firing_snd = other.end_firing_snd; + hud_tracking_snd = other.hud_tracking_snd; hud_locked_snd = other.hud_locked_snd; hud_in_flight_snd = other.hud_in_flight_snd; in_flight_play_type = other.in_flight_play_type; + tr_info = other.tr_info; + strcpy_s(icon_filename, other.icon_filename); strcpy_s(anim_filename, other.anim_filename); selection_effect = other.selection_effect; @@ -10317,25 +10351,38 @@ void weapon_info::clone(const weapon_info& other) fs2_effect_scanline_color = other.fs2_effect_scanline_color; fs2_effect_grid_density = other.fs2_effect_grid_density; fs2_effect_wireframe_color = other.fs2_effect_wireframe_color; + shield_impact_effect_radius = other.shield_impact_effect_radius; shield_impact_explosion_radius = other.shield_impact_explosion_radius; + impact_weapon_expl_effect = other.impact_weapon_expl_effect; dinky_impact_weapon_expl_effect = other.dinky_impact_weapon_expl_effect; flash_impact_weapon_expl_effect = other.flash_impact_weapon_expl_effect; + piercing_impact_effect = other.piercing_impact_effect; piercing_impact_secondary_effect = other.piercing_impact_secondary_effect; + conditional_impacts = other.conditional_impacts; + muzzle_effect = other.muzzle_effect; + state_effects = other.state_effects; + emp_intensity = other.emp_intensity; emp_time = other.emp_time; + recoil_modifier = other.recoil_modifier; + shudder_modifier = other.shudder_modifier; + weapon_reduce = other.weapon_reduce; afterburner_reduce = other.afterburner_reduce; + vamp_regen = other.vamp_regen; + tag_time = other.tag_time; tag_level = other.tag_level; + field_of_fire = other.field_of_fire; fof_spread_rate = other.fof_spread_rate; fof_reset_rate = other.fof_reset_rate; @@ -10343,6 +10390,7 @@ void weapon_info::clone(const weapon_info& other) firing_pattern = other.firing_pattern; shots = other.shots; cycle_multishot = other.cycle_multishot; + cs_num_fired = other.cs_num_fired; cs_radius = other.cs_radius; cs_twist = other.cs_twist; @@ -10350,6 +10398,7 @@ void weapon_info::clone(const weapon_info& other) cs_delay = other.cs_delay; cs_random_angle = other.cs_random_angle; cs_angle = other.cs_angle; + elec_time = other.elec_time; elec_eng_mult = other.elec_eng_mult; elec_weap_mult = other.elec_weap_mult; @@ -10357,15 +10406,20 @@ void weapon_info::clone(const weapon_info& other) elec_sensors_mult = other.elec_sensors_mult; elec_randomness = other.elec_randomness; elec_use_new_style = other.elec_use_new_style; + SSM_index = other.SSM_index; + lssm_warpout_delay = other.lssm_warpout_delay; lssm_warpin_delay = other.lssm_warpin_delay; lssm_stage5_vel = other.lssm_stage5_vel; lssm_warpin_radius = other.lssm_warpin_radius; lssm_lock_range = other.lssm_lock_range; lssm_warpeffect = other.lssm_warpeffect; + b_info = other.b_info; + particle_spewers = other.particle_spewers; + cm_aspect_effectiveness = other.cm_aspect_effectiveness; cm_heat_effectiveness = other.cm_heat_effectiveness; cm_effective_rad = other.cm_effective_rad; @@ -10379,38 +10433,55 @@ void weapon_info::clone(const weapon_info& other) cmeasure_use_firewait = other.cmeasure_use_firewait; cmeasure_failure_delay_multiplier_ai = other.cmeasure_failure_delay_multiplier_ai; cmeasure_success_delay_multiplier_ai = other.cmeasure_success_delay_multiplier_ai; + weapon_submodel_rotate_accell = other.weapon_submodel_rotate_accell; weapon_submodel_rotate_vel = other.weapon_submodel_rotate_vel; + damage_type_idx = other.damage_type_idx; damage_type_idx_sav = other.damage_type_idx_sav; + armor_type_idx = other.armor_type_idx; + alpha_max = other.alpha_max; alpha_min = other.alpha_min; alpha_cycle = other.alpha_cycle; + weapon_hitpoints = other.weapon_hitpoints; + burst_shots = other.burst_shots; burst_delay = other.burst_delay; burst_flags = other.burst_flags; + thruster_flame = other.thruster_flame; thruster_glow = other.thruster_glow; thruster_glow_factor = other.thruster_glow_factor; + target_lead_scaler = other.target_lead_scaler; num_targeting_priorities = other.num_targeting_priorities; memcpy(targeting_priorities, other.targeting_priorities, sizeof(targeting_priorities)); + failure_rate = other.failure_rate; CLONE_UNIQUE_PTR(failure_sub_name); failure_sub = other.failure_sub; + num_substitution_patterns = other.num_substitution_patterns; memcpy(weapon_substitution_pattern, other.weapon_substitution_pattern, sizeof(weapon_substitution_pattern)); memcpy(weapon_substitution_pattern_names, other.weapon_substitution_pattern_names, sizeof(weapon_substitution_pattern_names)); + score = other.score; + custom_data = other.custom_data; + custom_strings = other.custom_strings; + impact_decal = other.impact_decal; + on_create_program = other.on_create_program; + animations = other.animations; + weapon_launch_curves = other.weapon_launch_curves; weapon_curves = other.weapon_curves; weapon_hit_curves = other.weapon_hit_curves; From c9930c27f99c4ba07067e6c3eccec98b18f0a740 Mon Sep 17 00:00:00 2001 From: Dustinspace217 Date: Mon, 12 Oct 2026 11:01:41 -0700 Subject: [PATCH 3/4] Add weapon templates A #Weapon Templates section in weapons.tbl and *-wep.tbm holds $Template: entries that take the full weapon syntax. A weapon starts from one with +Use Template:, or from an already parsed weapon with +Use Weapon as Template:; a template can use another template. The rules follow ship templates: a template is never a weapon, +nocreate and +remove on a template are ignored with a warning, duplicate and missing names warn, a template or weapon naming itself warns, and a weapon that already exists ignores a template request with a warning. A template may declare $Subtype:, in which case it is parsed exactly as a weapon in that section would be. Without one it is parsed outside any section: no section-dependent defaults or fix-ups are applied to it, and the section-only fields are still accepted. A weapon built from such a template gets its section's missile flag default; where the section would have supplied a value the template left unset (homing free flight time, ballistic rearm rate) the engine warns instead of overwriting, so an explicit template value is never replaced by a default. The clone copies the animation set, so the set is re-keyed to the new weapon's name as ship clones are; the weapon parse now sets the set's name before reading $Animations: so that re-keying has something to work from. A substitution pattern that named the template now names the derived weapon. An inherited $On Create: program survives a derived entry that does not restate it. Every parse-time side table (delayed SSM entries, the legacy particle-spew buffer, pending proximity type and class names) is keyed by a string that tells templates and weapons apart, copied under the new name on a clone and dropped for templates after post-parse. The proximity lists were keyed by index into Weapon_info, which a template does not have and which the type sort reorders before the lists are consumed; the name key fixes both. A numeric $SSM: on a derived weapon retires an inherited named entry. weapon_close() now clears Weapons_inited and the pending proximity lists so the tables can be parsed again, which the unit tests rely on. Fixes #7343 --- code/particle/ParticleEffect.h | 4 +- code/particle/volumes/ConeVolume.h | 2 +- code/weapon/weapons.cpp | 376 ++++++++++++++---- test/src/weapon/weapons.cpp | 298 ++++++++++++++ .../data/tables/test-wep.tbm | 15 + .../data/tables/test-wep.tbm | 7 + .../data/tables/test-wep.tbm | 6 + .../data/tables/test-wep.tbm | 6 + .../data/tables/test-wep.tbm | 13 + .../data/tables/test-wep.tbm | 32 ++ .../data/tables/test-wep.tbm | 37 ++ .../data/tables/test-wep.tbm | 13 + .../data/tables/test-wep.tbm | 32 ++ .../data/tables/test-wep.tbm | 18 + .../data/tables/test-wep.tbm | 12 + .../data/tables/test-wep.tbm | 12 + .../data/tables/test-wep.tbm | 14 + .../data/tables/test-wep.tbm | 36 ++ .../data/tables/test-wep.tbm | 54 +++ .../data/tables/test-wep.tbm | 14 + .../data/tables/test-wep.tbm | 12 + .../data/tables/test-wep.tbm | 26 ++ .../data/tables/test-wep.tbm | 13 + .../data/tables/test-wep.tbm | 14 + .../data/tables/test-wep.tbm | 16 + .../data/tables/test-wep.tbm | 26 ++ .../data/tables/test-wep.tbm | 12 + .../data/tables/test-wep.tbm | 13 + .../data/tables/test-wep.tbm | 13 + .../data/tables/test-wep.tbm | 24 ++ .../data/tables/test-wep.tbm | 14 + .../data/tables/test-wep.tbm | 20 + .../data/tables/test-wep.tbm | 29 ++ .../data/tables/test-wep.tbm | 6 + 34 files changed, 1154 insertions(+), 85 deletions(-) create mode 100644 test/test_data/weapon/both_template_directives_warn/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/duplicate_template_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/missing_template_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/missing_weapon_as_template_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/nocreate_with_template_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/secondary_from_untyped_template_detonates_on_expiration/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_applies_to_new_weapon/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_display_name_inherited/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_display_name_rebuilt/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_free_flight_time_kept/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_german_display_name_not_inherited/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_hashed_name_not_inherited/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_laser_color_kept/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_of_template/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_on_existing_weapon_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_proximity_lists_follow_name/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_same_name_as_weapon_keeps_ssm/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_self_reference_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_ssm_copied/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_ssm_numeric_override/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_substitution_follows_name/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_with_nocreate_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_with_remove_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/template_with_weapon_as_template_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/untyped_template_ballistic_rearm_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/untyped_template_free_flight_warns/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/untyped_template_keeps_ballistic/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/untyped_template_reloaded_per_batch_by_section/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/weapon_as_template/data/tables/test-wep.tbm create mode 100644 test/test_data/weapon/weapon_as_template_self_warns/data/tables/test-wep.tbm diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 72dd1d700bd..2f283f77a06 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -21,7 +21,7 @@ class EffectHost; //Due to parsing shenanigans in weapons, this needs a forward-declare here -int parse_weapon(int, bool, const char*); +int parse_weapon(int, bool, const char*, bool); namespace scripting::api { particle::ParticleEffectHandle getLegacyScriptingParticleEffect(int bitmap, bool reversed); } @@ -132,7 +132,7 @@ class ParticleEffect { friend struct effects::EffectAttachment; friend struct ParticleParse; friend class ParticleManager; - friend int ::parse_weapon(int, bool, const char*); + friend int ::parse_weapon(int, bool, const char*, bool); friend ParticleEffectHandle scripting::api::getLegacyScriptingParticleEffect(int bitmap, bool reversed); friend bool move_particle(float frametime, particle* part); friend bool render_particle(particle* part); diff --git a/code/particle/volumes/ConeVolume.h b/code/particle/volumes/ConeVolume.h index ff583bc73ac..d5d2d147a55 100644 --- a/code/particle/volumes/ConeVolume.h +++ b/code/particle/volumes/ConeVolume.h @@ -6,7 +6,7 @@ namespace particle { class ConeVolume : public ParticleVolume { - friend int ::parse_weapon(int, bool, const char*); + friend int ::parse_weapon(int, bool, const char*, bool); ::util::ParsedRandomFloatRange m_deviation; ::util::ParsedRandomFloatRange m_length; diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index a7d094b3648..f5b1ffa2960 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -89,8 +89,8 @@ SCP_vector Delayed_SSM_indices; // Temporary storage for proximity type/class names pending resolution after ship_init(). // Keyed by weapon_info_index (not name) so a duplicate-named entry can't clobber another's pending list. -static SCP_unordered_map> Pending_proximity_type_names; -static SCP_unordered_map> Pending_proximity_class_names; +static SCP_unordered_map> Pending_proximity_type_names; +static SCP_unordered_map> Pending_proximity_class_names; #ifndef NDEBUG @@ -102,6 +102,7 @@ static TIMESTAMP Weapon_flyby_sound_timer; weapon Weapons[MAX_WEAPONS]; SCP_vector Weapon_info; +static SCP_vector Weapon_templates; #define MISSILE_OBJ_USED (1<<0) // flag used in missile_obj struct #define MAX_MISSILE_OBJS MAX_WEAPONS // max number of missiles tracked in missile list @@ -382,6 +383,17 @@ int weapon_info_lookup(const char *name) return -1; } +static int weapon_template_lookup(const char *name) +{ + Assertion(name != nullptr, "NULL name passed to weapon_template_lookup"); + + for (auto it = Weapon_templates.cbegin(); it != Weapon_templates.cend(); ++it) + if (!stricmp(name, it->name)) + return (int)std::distance(Weapon_templates.cbegin(), it); + + return -1; +} + // Parse the weapon flags. void parse_wi_flags(weapon_info *weaponp) { @@ -500,7 +512,7 @@ void parse_wi_flags(weapon_info *weaponp) } if (weaponp->wi_flags[Weapon::Info_Flags::Local_ssm]) { - if (!weaponp->is_homing() || weaponp->subtype != WP_MISSILE) { + if (!weaponp->is_homing() || (weaponp->subtype != WP_MISSILE && weaponp->subtype != WP_UNUSED)) { Warning(LOCATION, "local ssm must be guided missile: %s", weaponp->name); } } @@ -645,9 +657,55 @@ struct pspew_legacy_parse_data { SCP_string particle_spew_anim; }; -static SCP_unordered_map> pspew_legacy_parse_data_buffer; +static SCP_unordered_map> pspew_legacy_parse_data_buffer; static bool pspew_do_warning = false; +// templates and weapons may share a name, so their parse-time side data must not share a key +static SCP_string weapon_parse_key(const char *name, bool is_template) +{ + return is_template ? SCP_string("$Template:") + name : SCP_string(name); +} + +static void weapon_copy_delayed_parse_data(const SCP_string& from, const SCP_string& to) +{ + auto ssm = Delayed_SSM_data.find(from); + if (ssm != Delayed_SSM_data.end()) { + if (Delayed_SSM_data.find(to) == Delayed_SSM_data.end()) + Delayed_SSM_names.emplace_back(to); + Delayed_SSM_data[to] = ssm->second; + } + + auto ssm_index = Delayed_SSM_indices_data.find(from); + if (ssm_index != Delayed_SSM_indices_data.end()) { + if (Delayed_SSM_indices_data.find(to) == Delayed_SSM_indices_data.end()) + Delayed_SSM_indices.emplace_back(to); + Delayed_SSM_indices_data[to] = ssm_index->second; + } + + auto pspew = pspew_legacy_parse_data_buffer.find(from); + if (pspew != pspew_legacy_parse_data_buffer.end()) + pspew_legacy_parse_data_buffer[to] = pspew->second; + + auto prox_type = Pending_proximity_type_names.find(from); + if (prox_type != Pending_proximity_type_names.end()) + Pending_proximity_type_names[to] = prox_type->second; + + auto prox_class = Pending_proximity_class_names.find(from); + if (prox_class != Pending_proximity_class_names.end()) + Pending_proximity_class_names[to] = prox_class->second; +} + +static void weapon_remove_delayed_parse_data(const SCP_string& name) +{ + Delayed_SSM_data.erase(name); + Delayed_SSM_names.erase(std::remove(Delayed_SSM_names.begin(), Delayed_SSM_names.end(), name), Delayed_SSM_names.end()); + Delayed_SSM_indices_data.erase(name); + Delayed_SSM_indices.erase(std::remove(Delayed_SSM_indices.begin(), Delayed_SSM_indices.end(), name), Delayed_SSM_indices.end()); + pspew_legacy_parse_data_buffer.erase(name); + Pending_proximity_type_names.erase(name); + Pending_proximity_class_names.erase(name); +} + static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legacy_parse_data& pspew_buffer, const weapon_info* wip) { auto particle_spew_count = static_cast(pspew_buffer.particle_spew_count); float particle_spew_spawns_per_second = 1000.f / static_cast(pspew_buffer.particle_spew_time); @@ -767,18 +825,26 @@ static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legac * Parse the information for a specific ship type. * Return weapon index if successful, otherwise return -1 */ -int parse_weapon(int subtype, bool replace, const char *filename) +int parse_weapon(int subtype, bool replace, const char *filename, bool is_template) { char buf[NAME_LENGTH]; char fname[NAME_LENGTH]; + char template_name[NAME_LENGTH]; weapon_info *wip = nullptr; int iff, idx; int primary_rearm_rate_specified=0; bool first_time = false; bool create_if_not_found = true; bool remove_weapon = false; - - required_string("$Name:"); + bool new_entry = false; + bool use_template = false; + bool use_weapon = false; + bool subtype_defaults = false; + bool untyped_template = false; + bool from_template = false; + SCP_string parse_key; + + required_string(is_template ? "$Template:" : "$Name:"); stuff_string(fname, F_NAME, NAME_LENGTH); // Remove @ symbol -- these used to denote weapons that would only be parsed in demo builds @@ -789,81 +855,186 @@ int parse_weapon(int subtype, bool replace, const char *filename) diag_printf("Weapon name -- %s\n", fname); if(optional_string("+nocreate")) { - if(!replace) { - Warning(LOCATION, "+nocreate flag used for weapon in non-modular table"); + if (is_template) { + Warning(LOCATION, "+nocreate flag used on weapon template '%s'. Weapon templates can not be modified. Ignoring +nocreate.", fname); + } else { + if(!replace) { + Warning(LOCATION, "+nocreate flag used for weapon in non-modular table"); + } + create_if_not_found = false; } - create_if_not_found = false; } // check first if this is on the remove blacklist auto it = std::find(Removed_weapons.begin(), Removed_weapons.end(), fname); - if (it != Removed_weapons.end()) { + if (!is_template && it != Removed_weapons.end()) { remove_weapon = true; } - + // we might have a remove tag if (optional_string("+remove")) { - if (!replace) { - Warning(LOCATION, "+remove flag used for weapon in non-modular table"); - } - if (!remove_weapon) { - Removed_weapons.push_back(fname); - remove_weapon = true; + if (is_template) { + Warning(LOCATION, "+remove flag used on weapon template '%s'. Weapon templates can not be removed. Ignoring +remove.", fname); + } else { + if (!replace) { + Warning(LOCATION, "+remove flag used for weapon in non-modular table"); + } + if (!remove_weapon) { + Removed_weapons.emplace_back(fname); + remove_weapon = true; + } } } - //Check if weapon exists already - int w_id = weapon_info_lookup(fname); - - // maybe remove it - if (remove_weapon) - { - if (w_id >= 0) { - mprintf(("Removing previously parsed weapon '%s'\n", fname)); - Weapon_info.erase(Weapon_info.begin() + w_id); - } + if (optional_string("+Use Template:")) { + stuff_string(template_name, F_NAME, NAME_LENGTH); + use_template = true; + } - if (!skip_to_start_of_string_either("$Name:", "#End")) { - error_display(1, "Missing [#End] or [$Name] after weapon class %s", fname); + if (optional_string("+Use Weapon as Template:")) { + if (is_template) { + Warning(LOCATION, "'+Use Weapon as Template:' is not supported on weapon template '%s', ignoring it", fname); + stuff_string(buf, F_NAME, NAME_LENGTH); + } else if (use_template) { + Warning(LOCATION, "Both '+Use Template:' and '+Use Weapon as Template:' were specified for weapon '%s', ignoring '+Use Weapon as Template:'", fname); + stuff_string(buf, F_NAME, NAME_LENGTH); + } else { + stuff_string(template_name, F_NAME, NAME_LENGTH); + use_weapon = true; } - return -1; } - // an entry for this weapon exists - if (w_id >= 0) + int w_id = -1; + + if (is_template) { - wip = &Weapon_info[w_id]; - if (!replace) - { - Warning(LOCATION, "Weapon name %s already exists in weapons.tbl. All weapon names must be unique; the second entry has been skipped", fname); - if (!skip_to_start_of_string_either("$Name:", "#End")) { - error_display(1, "Missing [#End] or [$Name] after duplicate weapon %s", fname); + if (weapon_template_lookup(fname) >= 0) { + Warning(LOCATION, "Weapon template %s already exists. All weapon template names must be unique; the second entry has been skipped", fname); + if (!skip_to_start_of_string_either("$Template:", "#End")) { + error_display(1, "Missing [#End] or [$Template] after duplicate weapon template %s", fname); } return -1; } + + w_id = static_cast(Weapon_templates.size()); + Weapon_templates.emplace_back(); + wip = &Weapon_templates.back(); + first_time = true; + new_entry = true; + + strcpy_s(wip->name, fname); } - // an entry does not exist else { - // Don't create weapon if it has +nocreate and is in a modular table. - if (!create_if_not_found && replace) + w_id = weapon_info_lookup(fname); + + // maybe remove it + if (remove_weapon) { - if (!skip_to_start_of_string_either("$Name:", "#End")) { - error_display(1, "Missing [#End] or [$Name] after weapon %s", fname); + if (w_id >= 0) { + mprintf(("Removing previously parsed weapon '%s'\n", fname)); + Weapon_info.erase(Weapon_info.begin() + w_id); } + if (!skip_to_start_of_string_either("$Name:", "#End")) { + error_display(1, "Missing [#End] or [$Name] after weapon class %s", fname); + } return -1; } - w_id = weapon_info_size(); - Weapon_info.push_back(weapon_info()); - wip = &Weapon_info.back(); - first_time = true; + // an entry for this weapon exists + if (w_id >= 0) + { + wip = &Weapon_info[w_id]; + if (!replace) + { + Warning(LOCATION, "Weapon name %s already exists in weapons.tbl. All weapon names must be unique; the second entry has been skipped", fname); + if (!skip_to_start_of_string_either("$Name:", "#End")) { + error_display(1, "Missing [#End] or [$Name] after duplicate weapon %s", fname); + } + return -1; + } - strcpy_s(wip->name, fname); + if (use_template || use_weapon) { + Warning(LOCATION, "A template was specified for weapon '%s', which already exists. Templates can only be used when a weapon is first defined; ignoring template request...", fname); + use_template = false; + use_weapon = false; + } + } + // an entry does not exist + else + { + // Don't create weapon if it has +nocreate and is in a modular table. + if (!create_if_not_found && replace) + { + if (use_template || use_weapon) { + Warning(LOCATION, "Both '+nocreate' and a template were specified for weapon '%s', ignoring template request...", fname); + } + + if (!skip_to_start_of_string_either("$Name:", "#End")) { + error_display(1, "Missing [#End] or [$Name] after weapon %s", fname); + } + + return -1; + } + + w_id = weapon_info_size(); + Weapon_info.emplace_back(); + wip = &Weapon_info.back(); + first_time = true; + new_entry = true; + + strcpy_s(wip->name, fname); + } + } + + if (use_template || use_weapon) + { + const weapon_info *source = nullptr; + + if (use_template) { + int template_id = weapon_template_lookup(template_name); + if (template_id < 0) { + Warning(LOCATION, "Unable to find weapon template '%s' requested by %s '%s', ignoring template request...", template_name, is_template ? "weapon template" : "weapon", fname); + } else if (is_template && template_id == w_id) { + Warning(LOCATION, "Weapon template '%s' requested itself as a template, ignoring template request...", fname); + } else { + source = &Weapon_templates[template_id]; + } + } else { + int source_id = weapon_info_lookup(template_name); + if (source_id < 0) { + Warning(LOCATION, "Unable to find weapon '%s' requested as a template by weapon '%s', ignoring template request...", template_name, fname); + } else if (source_id == w_id) { + Warning(LOCATION, "Weapon '%s' requested itself as a template, ignoring template request...", fname); + } else { + source = &Weapon_info[source_id]; + } + } + + if (source != nullptr) { + // a template with no $Subtype: carries no section defaults; the derived weapon's section adds the missile flag and warns about the rest + subtype_defaults = !is_template && (source->subtype == WP_UNUSED); + wip->clone(*source); + first_time = false; + strcpy_s(wip->name, fname); + for (size_t j = 0; j < wip->num_substitution_patterns; ++j) { + if (!stricmp(wip->weapon_substitution_pattern_names[j], source->name)) { + strcpy_s(wip->weapon_substitution_pattern_names[j], wip->name); + } + } + wip->animations.changeShipName(wip->name); + weapon_copy_delayed_parse_data(weapon_parse_key(source->name, use_template), weapon_parse_key(wip->name, is_template)); + from_template = true; + } + } + parse_key = weapon_parse_key(wip->name, is_template); + + if (new_entry && !is_template) + { // if this name has a hash, create a default display name - if (get_pointer_to_first_hash_symbol(wip->name)) { + if (!wip->has_display_name() && get_pointer_to_first_hash_symbol(wip->name)) { wip->display_name = wip->name; end_string_at_first_hash_symbol(wip->display_name, true); consolidate_double_characters(wip->display_name, '#'); @@ -899,19 +1070,24 @@ int parse_weapon(int subtype, bool replace, const char *filename) Warning(LOCATION, "Unknown subtype on weapon '%s'", wip->name); } } - else if(wip->subtype != WP_UNUSED && !first_time) + else if(!is_template && wip->subtype != WP_UNUSED && !first_time) { if(wip->subtype != subtype) { Warning(LOCATION, "Type of weapon %s entry does not agree with original entry type.", wip->name); } } - else + else if (!is_template) { wip->subtype = subtype; } + if (is_template) { + subtype = wip->subtype; + untyped_template = (subtype == WP_UNUSED); + } + // assign subtype-specific flags - if (first_time) + if (first_time || subtype_defaults) { if (wip->subtype == WP_MISSILE) wip->wi_flags.set(Weapon::Info_Flags::Detonate_on_expiration); @@ -1375,14 +1551,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) } } - const int wi_index = static_cast(wip - Weapon_info.data()); - if (optional_string("+Proximity Type:")) { - stuff_string_list(Pending_proximity_type_names[wi_index]); + stuff_string_list(Pending_proximity_type_names[parse_key]); } if (optional_string("+Proximity Class:")) { - stuff_string_list(Pending_proximity_class_names[wi_index]); + stuff_string_list(Pending_proximity_class_names[parse_key]); } // Launcher-relative filter: hostile/friendly/neutral, combinable. Default (no entry) = any relation. @@ -1952,13 +2126,15 @@ int parse_weapon(int subtype, bool replace, const char *filename) if(optional_string("$Free Flight Time:")) { stuff_float(&(wip->free_flight_time)); - } else if(first_time && wip->is_homing()) { + } else if(first_time && !untyped_template && wip->is_homing()) { if (subtype == WP_LASER) { wip->free_flight_time = HOMING_DEFAULT_PRIMARY_FREE_FLIGHT_TIME; } else { wip->free_flight_time = HOMING_DEFAULT_FREE_FLIGHT_TIME; } + } else if (subtype_defaults && wip->is_homing() && wip->free_flight_time == 0.0f) { + Warning(LOCATION, "Weapon '%s' was built from a template with no $Subtype: and sets no $Free Flight Time:; keeping %.2f from the template", wip->name, wip->free_flight_time); } if (optional_string("$Free Flight Speed:")) { @@ -2100,7 +2276,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) } } // This setting requires a default if not specified, otherwise, rearming will break. - Cyborg17 - if (wip->reloaded_per_batch == -1) { + if (wip->reloaded_per_batch == -1 && !untyped_template) { if (subtype == WP_MISSILE) { wip->reloaded_per_batch = REARM_NUM_MISSILES_PER_BATCH; } else { @@ -2182,6 +2358,10 @@ int parse_weapon(int subtype, bool replace, const char *filename) Warning(LOCATION, "$Rearm Rate for ballistic primary %s not specified. Defaulting to 100...\n", wip->name); wip->rearm_rate = 100.0f; } + else if (!primary_rearm_rate_specified && subtype_defaults && wip->rearm_rate == 1.0f) + { + Warning(LOCATION, "Ballistic primary %s was built from a template with no $Subtype: and sets no $Rearm Rate; keeping %.2f from the template\n", wip->name, wip->rearm_rate); + } } // not ballistic else @@ -2195,7 +2375,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) } // secondary - else + else if (!untyped_template) { // ballistic if (wip->wi_flags[Weapon::Info_Flags::Ballistic]) @@ -2215,7 +2395,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) } // also make sure secondaries and ballistic primaries do not have 0 cargo size - if (subtype == WP_MISSILE || wip->wi_flags[Weapon::Info_Flags::Ballistic]) + if ((subtype == WP_MISSILE || wip->wi_flags[Weapon::Info_Flags::Ballistic]) && !untyped_template) { if (wip->cargo_size == 0.0f) { @@ -2654,7 +2834,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->state_effects[WeaponState::NORMAL] = effetIdx; } - if (wip->subtype == WP_MISSILE) + if (wip->subtype == WP_MISSILE || untyped_template) { if (optional_string("$Freeflight Effect:")) { auto effetIdx = particle::util::parseEffect(wip->name); @@ -3870,7 +4050,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->particle_spewers[spew_index] = particle::util::parseEffect(wip->name); } else { // we have a valid index, now parse the spewer already - auto& pspew_buffer = pspew_legacy_parse_data_buffer[WEAPON_INFO_INDEX(wip)][spew_index]; + auto& pspew_buffer = pspew_legacy_parse_data_buffer[parse_key][spew_index]; if (pspew_buffer.particle_spew_type == PSPEW_NONE) { //This must be an uninitialized effect, store defaults. pspew_buffer.particle_spew_count = 1; @@ -3973,7 +4153,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) if (wip->particle_spewers.empty()) { wip->particle_spewers.emplace_back(particle::ParticleEffectHandle::invalid()); } - auto& pspew_buffer = pspew_legacy_parse_data_buffer[WEAPON_INFO_INDEX(wip)][0]; + auto& pspew_buffer = pspew_legacy_parse_data_buffer[parse_key][0]; pspew_buffer.particle_spew_count = 1; pspew_buffer.particle_spew_time = 25; pspew_buffer.particle_spew_vel = 0.4f; @@ -4004,17 +4184,21 @@ int parse_weapon(int subtype, bool replace, const char *filename) temp_data.filename = filename; temp_data.linenum = get_line_num(); temp_data.ssm_entry = fname; - if (Delayed_SSM_data.find(wip->name) == Delayed_SSM_data.end()) - Delayed_SSM_names.push_back(wip->name); - Delayed_SSM_data[wip->name] = std::move(temp_data); + if (Delayed_SSM_data.find(parse_key) == Delayed_SSM_data.end()) + Delayed_SSM_names.push_back(parse_key); + Delayed_SSM_data[parse_key] = std::move(temp_data); + Delayed_SSM_indices_data.erase(parse_key); + Delayed_SSM_indices.erase(std::remove(Delayed_SSM_indices.begin(), Delayed_SSM_indices.end(), parse_key), Delayed_SSM_indices.end()); } else { // We'll still want to validate the index later. -MageKing17 delayed_ssm_index_data temp_data; temp_data.filename = filename; temp_data.linenum = get_line_num(); - if (Delayed_SSM_indices_data.find(wip->name) == Delayed_SSM_indices_data.end()) - Delayed_SSM_indices.push_back(wip->name); - Delayed_SSM_indices_data[wip->name] = std::move(temp_data); + if (Delayed_SSM_indices_data.find(parse_key) == Delayed_SSM_indices_data.end()) + Delayed_SSM_indices.push_back(parse_key); + Delayed_SSM_indices_data[parse_key] = std::move(temp_data); + Delayed_SSM_data.erase(parse_key); + Delayed_SSM_names.erase(std::remove(Delayed_SSM_names.begin(), Delayed_SSM_names.end(), parse_key), Delayed_SSM_names.end()); } }// SSM index -Bobboau @@ -4208,7 +4392,7 @@ int parse_weapon(int subtype, bool replace, const char *filename) } // This only works with lasters and missiles for now - if (wip->subtype == WP_MISSILE || wip->subtype == WP_LASER) { + if (wip->subtype == WP_MISSILE || wip->subtype == WP_LASER || untyped_template) { // We always have an object we are attached to flagset contexts{actions::ProgramContextFlags::HasObject}; @@ -4217,14 +4401,18 @@ int parse_weapon(int subtype, bool replace, const char *filename) contexts.set(actions::ProgramContextFlags::HasSubobject, true); } - wip->on_create_program = actions::ProgramSet::parseProgramSet("$On Create:", contexts); + if (!from_template || check_for_string("$On Create:")) { + wip->on_create_program = actions::ProgramSet::parseProgramSet("$On Create:", contexts); + } } if (optional_string("$Animations:")) { + wip->animations.changeShipName(wip->name); animation::ModelAnimationParseHelper::parseAnimsetInfo(wip->animations, 'w', wip->name); } if (optional_string("$Driven Animations:")) { + wip->animations.changeShipName(wip->name); animation::ModelAnimationParseHelper::parseAnimsetInfoDrivers(wip->animations, 'w', wip->name, animation::parse_object_property_driver_source); } @@ -4395,11 +4583,21 @@ void parse_weaponstbl(const char *filename) read_file_text(filename, CF_TYPE_TABLES); reset_parse(); + if (optional_string("#Weapon Templates")) + { + while (required_string_either("#End", "$Template:")) { + if (parse_weapon(WP_UNUSED, Parsing_modular_table, filename, true) < 0) { + continue; + } + } + required_string("#End"); + } + if (optional_string("#Primary Weapons")) { while (required_string_either("#End", "$Name:")) { // AL 28-3-98: If parse_weapon() fails, try next .tbl weapon - if (parse_weapon(WP_LASER, Parsing_modular_table, filename) < 0) { + if (parse_weapon(WP_LASER, Parsing_modular_table, filename, false) < 0) { continue; } } @@ -4410,7 +4608,7 @@ void parse_weaponstbl(const char *filename) { while (required_string_either("#End", "$Name:")) { // AL 28-3-98: If parse_weapon() fails, try next .tbl weapon - if (parse_weapon(WP_MISSILE, Parsing_modular_table, filename) < 0) { + if (parse_weapon(WP_MISSILE, Parsing_modular_table, filename, false) < 0) { continue; } } @@ -4421,7 +4619,7 @@ void parse_weaponstbl(const char *filename) { while (required_string_either("#End", "$Name:")) { // AL 28-3-98: If parse_weapon() fails, try next .tbl weapon - if (parse_weapon(WP_BEAM, Parsing_modular_table, filename) < 0) { + if (parse_weapon(WP_BEAM, Parsing_modular_table, filename, false) < 0) { continue; } } @@ -4432,7 +4630,7 @@ void parse_weaponstbl(const char *filename) { while (required_string_either("#End", "$Name:")) { - int idx = parse_weapon(WP_MISSILE, Parsing_modular_table, filename); + int idx = parse_weapon(WP_MISSILE, Parsing_modular_table, filename, false); if (idx < 0) { continue; @@ -5107,16 +5305,22 @@ void weapon_do_post_parse() // translate all spawn type weapons to referrnce the appropriate spawned weapon entry translate_spawn_types(); + + for (const auto& wt : Weapon_templates) { + weapon_remove_delayed_parse_data(weapon_parse_key(wt.name, true)); + } + Weapon_templates.clear(); } // Called after ship_init() to resolve proximity ship type/class names into indices. void weapon_post_ship_init() { - const int num_weapons = static_cast(Weapon_info.size()); - for (const auto& entry : Pending_proximity_type_names) { - const int wi_index = entry.first; - Assertion(wi_index >= 0 && wi_index < num_weapons, "Pending proximity type entry has out-of-range weapon_info index %d (num_weapons=%d).", wi_index, num_weapons); + const int wi_index = weapon_info_lookup(entry.first.c_str()); + if (wi_index < 0) { + Warning(LOCATION, "Weapon '%s' has pending +Proximity Type entries but no weapon_info entry.\n", entry.first.c_str()); + continue; + } weapon_info& wip = Weapon_info[wi_index]; for (const SCP_string& type_name : entry.second) { int idx = ship_type_name_lookup(type_name.c_str()); @@ -5128,8 +5332,11 @@ void weapon_post_ship_init() } for (const auto& entry : Pending_proximity_class_names) { - const int wi_index = entry.first; - Assertion(wi_index >= 0 && wi_index < num_weapons, "Pending proximity class entry has out-of-range weapon_info index %d (num_weapons=%d).", wi_index, num_weapons); + const int wi_index = weapon_info_lookup(entry.first.c_str()); + if (wi_index < 0) { + Warning(LOCATION, "Weapon '%s' has pending +Proximity Class entries but no weapon_info entry.\n", entry.first.c_str()); + continue; + } weapon_info& wip = Weapon_info[wi_index]; for (const SCP_string& class_name : entry.second) { int idx = ship_info_lookup(class_name.c_str()); @@ -5155,6 +5362,7 @@ void weapon_init() // parse weapons.tbl Removed_weapons.clear(); Weapon_info.clear(); + Weapon_templates.clear(); parse_weaponstbl("weapons.tbl"); parse_modular_table(NOX("*-wep.tbm"), parse_weaponstbl); @@ -5192,6 +5400,10 @@ void weapon_close() } Spawn_names.clear(); + Pending_proximity_type_names.clear(); + Pending_proximity_class_names.clear(); + + Weapons_inited = false; } /** diff --git a/test/src/weapon/weapons.cpp b/test/src/weapon/weapons.cpp index d507b065d49..98bb783abc2 100644 --- a/test/src/weapon/weapons.cpp +++ b/test/src/weapon/weapons.cpp @@ -5,8 +5,22 @@ #include "util/test_util.h" #include "utils/string_utils.h" +#include "localization/localize.h" #include "weapon/weapon.h" +extern SCP_vector Delayed_SSM_names; +extern SCP_vector Delayed_SSM_indices; + +static void expect_init_warning(const char* fragment) +{ + try { + weapon_init(); + FAIL() << "weapon_init() did not warn"; + } catch (const os::dialogs::WarningException& e) { + EXPECT_NE(SCP_string(e.what()).find(fragment), SCP_string::npos) << "warning was: " << e.what(); + } +} + class WeaponsParseTest : public test::FSTestFixture { public: WeaponsParseTest() : test::FSTestFixture(INIT_CFILE) { @@ -19,6 +33,7 @@ class WeaponsParseTest : public test::FSTestFixture { } void TearDown() override { weapon_close(); + Weapon_info.clear(); test::FSTestFixture::TearDown(); } @@ -36,6 +51,289 @@ TEST_F(WeaponsParseTest, description_too_many_lines) { ASSERT_THROW(weapon_init(), os::dialogs::WarningException); } +TEST_F(WeaponsParseTest, template_applies_to_new_weapon) { + weapon_init(); + + EXPECT_LT(weapon_info_lookup("Base Laser"), 0); + + int derived = weapon_info_lookup("Derived Laser"); + ASSERT_GE(derived, 0); + EXPECT_EQ(Weapon_info[derived].damage, 9.0f); + EXPECT_EQ(Weapon_info[derived].max_speed, 500.0f); + EXPECT_STREQ(Weapon_info[derived].title, "Base"); + EXPECT_EQ(Weapon_info[derived].subtype, WP_LASER); + + int plain = weapon_info_lookup("Plain Laser"); + ASSERT_GE(plain, 0); + EXPECT_EQ(Weapon_info[plain].damage, 7.0f); + EXPECT_STREQ(Weapon_info[plain].name, "Plain Laser"); +} + +TEST_F(WeaponsParseTest, template_of_template) { + weapon_init(); + + int leaf = weapon_info_lookup("Leaf"); + ASSERT_GE(leaf, 0); + EXPECT_EQ(Weapon_info[leaf].max_speed, 321.0f); + EXPECT_EQ(Weapon_info[leaf].damage, 3.0f); + EXPECT_LT(weapon_info_lookup("Root"), 0); + EXPECT_LT(weapon_info_lookup("Branch"), 0); +} + +TEST_F(WeaponsParseTest, weapon_as_template) { + weapon_init(); + + int alpha = weapon_info_lookup("Alpha"); + int beta = weapon_info_lookup("Beta"); + ASSERT_GE(alpha, 0); + ASSERT_GE(beta, 0); + EXPECT_EQ(Weapon_info[beta].max_speed, 640.0f); + EXPECT_EQ(Weapon_info[beta].damage, 2.0f); + EXPECT_EQ(Weapon_info[alpha].damage, 4.0f); + EXPECT_STREQ(Weapon_info[beta].name, "Beta"); +} + +TEST_F(WeaponsParseTest, secondary_from_untyped_template_detonates_on_expiration) { + weapon_init(); + + int derived = weapon_info_lookup("Derived Missile"); + ASSERT_GE(derived, 0); + EXPECT_EQ(Weapon_info[derived].subtype, WP_MISSILE); + EXPECT_TRUE(Weapon_info[derived].wi_flags[Weapon::Info_Flags::Detonate_on_expiration]); +} + +TEST_F(WeaponsParseTest, template_display_name_rebuilt) { + weapon_init(); + + int derived = weapon_info_lookup("Derived#Laser"); + ASSERT_GE(derived, 0); + EXPECT_TRUE(Weapon_info[derived].has_display_name()); + EXPECT_EQ(Weapon_info[derived].display_name, "Derived"); +} + +TEST_F(WeaponsParseTest, missing_template_warns) { + DEBUG_TEST(); + + expect_init_warning("Unable to find weapon template 'No Such Template'"); +} + +TEST_F(WeaponsParseTest, template_self_reference_warns) { + DEBUG_TEST(); + + expect_init_warning("requested itself as a template"); +} + +TEST_F(WeaponsParseTest, template_with_nocreate_warns) { + DEBUG_TEST(); + + expect_init_warning("+nocreate flag used on weapon template 'Base Laser'"); +} + +TEST_F(WeaponsParseTest, template_on_existing_weapon_warns) { + DEBUG_TEST(); + + for (int attempt = 0; attempt < 2; ++attempt) { + expect_init_warning("A template was specified for weapon 'Alpha', which already exists"); + weapon_close(); + } +} + +TEST_F(WeaponsParseTest, template_free_flight_time_kept) { + weapon_init(); + + int d = weapon_info_lookup("Seeker Derived"); + ASSERT_GE(d, 0); + EXPECT_TRUE(Weapon_info[d].is_homing()); + EXPECT_EQ(Weapon_info[d].free_flight_time, 0.3f); +} + +TEST_F(WeaponsParseTest, untyped_template_free_flight_warns) { +#ifndef NDEBUG + expect_init_warning("sets no $Free Flight Time:; keeping 0.00 from the template"); +#else + weapon_init(); + + int p = weapon_info_lookup("Seeker Primary"); + int s = weapon_info_lookup("Seeker Secondary"); + ASSERT_GE(p, 0); + ASSERT_GE(s, 0); + EXPECT_EQ(Weapon_info[p].free_flight_time, 0.0f); + EXPECT_EQ(Weapon_info[s].free_flight_time, 0.0f); +#endif +} + +TEST_F(WeaponsParseTest, template_laser_color_kept) { + weapon_init(); + + int d = weapon_info_lookup("Tinted Derived"); + ASSERT_GE(d, 0); + EXPECT_EQ(Weapon_info[d].laser_color_2.red, 0); + EXPECT_EQ(Weapon_info[d].laser_color_2.blue, 255); +} + +TEST_F(WeaponsParseTest, template_ssm_copied) { + weapon_init(); + + auto listed = [](const char* n) { + return std::find(Delayed_SSM_names.begin(), Delayed_SSM_names.end(), n) != Delayed_SSM_names.end(); + }; + EXPECT_TRUE(listed("Artillery Derived")); + EXPECT_FALSE(listed("Artillery Base")); + EXPECT_FALSE(listed("$Template:Artillery Base")); + Delayed_SSM_names.clear(); +} + +TEST_F(WeaponsParseTest, template_proximity_lists_follow_name) { + DEBUG_TEST(); + + weapon_init(); + + try { + weapon_post_ship_init(); + FAIL() << "no warning: the proximity list was lost"; + } catch (const os::dialogs::AssertException& e) { + FAIL() << "garbage index: " << e.what(); + } catch (const os::dialogs::WarningException& e) { + EXPECT_NE(SCP_string(e.what()).find("Weapon 'Flak Derived': +Proximity Type entry 'fighter' not found"), SCP_string::npos) << e.what(); + } +} + +TEST_F(WeaponsParseTest, template_same_name_as_weapon_keeps_ssm) { + weapon_init(); + + EXPECT_NE(std::find(Delayed_SSM_names.begin(), Delayed_SSM_names.end(), "Trebuchet"), Delayed_SSM_names.end()); +#ifndef NDEBUG + try { + validate_SSM_entries(); + FAIL() << "validate_SSM_entries saw no entry for Trebuchet"; + } catch (const os::dialogs::WarningException& e) { + EXPECT_NE(SCP_string(e.what()).find("SSM entry 'Trebuchet Strike' in specification for Trebuchet"), SCP_string::npos) << e.what(); + } +#endif + Delayed_SSM_names.clear(); +} + +TEST_F(WeaponsParseTest, template_with_remove_warns) { + DEBUG_TEST(); + + expect_init_warning("+remove flag used on weapon template 'Doomed'"); +} + +TEST_F(WeaponsParseTest, template_display_name_inherited) { + weapon_init(); + + int d = weapon_info_lookup("Derived#Laser"); + ASSERT_GE(d, 0); + EXPECT_EQ(Weapon_info[d].display_name, "Shown"); +} + +TEST_F(WeaponsParseTest, template_hashed_name_not_inherited) { + weapon_init(); + + int d = weapon_info_lookup("Subach#Mk2"); + ASSERT_GE(d, 0); + EXPECT_EQ(Weapon_info[d].display_name, "Subach"); +} + +TEST_F(WeaponsParseTest, template_german_display_name_not_inherited) { + Lcl_gr = 1; + weapon_init(); + Lcl_gr = 0; + + int d = weapon_info_lookup("Derived Laser"); + ASSERT_GE(d, 0); + EXPECT_TRUE(Weapon_info[d].has_display_name()); + EXPECT_EQ(Weapon_info[d].display_name, "Derived Laser"); +} + +TEST_F(WeaponsParseTest, weapon_as_template_self_warns) { + DEBUG_TEST(); + + expect_init_warning("Weapon 'Foo' requested itself as a template"); +} + +TEST_F(WeaponsParseTest, template_with_weapon_as_template_warns) { +#ifndef NDEBUG + expect_init_warning("'+Use Weapon as Template:' is not supported on weapon template 'Base Laser'"); +#else + weapon_init(); + + EXPECT_GE(weapon_info_lookup("Survivor"), 0); +#endif +} + +TEST_F(WeaponsParseTest, untyped_template_keeps_ballistic) { + weapon_init(); + + int d = weapon_info_lookup("Gun Derived"); + ASSERT_GE(d, 0); + EXPECT_TRUE(Weapon_info[d].wi_flags[Weapon::Info_Flags::Ballistic]); + EXPECT_EQ(Weapon_info[d].rearm_rate, 1.0f / 50.0f); +} + +TEST_F(WeaponsParseTest, untyped_template_ballistic_rearm_warns) { + DEBUG_TEST(); + + expect_init_warning("Ballistic primary Gun Derived was built from a template with no $Subtype: and sets no $Rearm Rate; keeping 1.00"); +} + +TEST_F(WeaponsParseTest, template_substitution_follows_name) { + weapon_init(); + + int d = weapon_info_lookup("Sub Derived"); + int other = weapon_info_lookup("Sub Other"); + ASSERT_GE(d, 0); + ASSERT_GE(other, 0); + ASSERT_EQ(Weapon_info[d].num_substitution_patterns, 2u); + EXPECT_EQ(Weapon_info[d].weapon_substitution_pattern[0], d); + EXPECT_EQ(Weapon_info[d].weapon_substitution_pattern[1], other); +} + +TEST_F(WeaponsParseTest, template_ssm_numeric_override) { + weapon_init(); + + auto listed = [](const SCP_vector& v, const char* n) { return std::find(v.begin(), v.end(), n) != v.end(); }; + EXPECT_FALSE(listed(Delayed_SSM_names, "Artillery Numeric")); + EXPECT_TRUE(listed(Delayed_SSM_indices, "Artillery Numeric")); + Delayed_SSM_names.clear(); + Delayed_SSM_indices.clear(); +} + +TEST_F(WeaponsParseTest, both_template_directives_warn) { + DEBUG_TEST(); + + expect_init_warning("ignoring '+Use Weapon as Template:'"); +} + +TEST_F(WeaponsParseTest, duplicate_template_warns) { + DEBUG_TEST(); + + expect_init_warning("Weapon template Twin already exists"); +} + +TEST_F(WeaponsParseTest, nocreate_with_template_warns) { + DEBUG_TEST(); + + expect_init_warning("Both '+nocreate' and a template were specified for weapon 'Ghost'"); +} + +TEST_F(WeaponsParseTest, missing_weapon_as_template_warns) { + DEBUG_TEST(); + + expect_init_warning("Unable to find weapon 'Nope' requested as a template by weapon 'Orphan'"); +} + +TEST_F(WeaponsParseTest, untyped_template_reloaded_per_batch_by_section) { + weapon_init(); + + int p = weapon_info_lookup("Any Primary"); + int s = weapon_info_lookup("Any Secondary"); + ASSERT_GE(p, 0); + ASSERT_GE(s, 0); + EXPECT_EQ(Weapon_info[p].reloaded_per_batch, 100); + EXPECT_EQ(Weapon_info[s].reloaded_per_batch, 4); +} + TEST(WeaponInfoCloneTest, copies_members) { weapon_info src; strcpy_s(src.name, "Template Laser"); diff --git a/test/test_data/weapon/both_template_directives_warn/data/tables/test-wep.tbm b/test/test_data/weapon/both_template_directives_warn/data/tables/test-wep.tbm new file mode 100644 index 00000000000..7e17af153e8 --- /dev/null +++ b/test/test_data/weapon/both_template_directives_warn/data/tables/test-wep.tbm @@ -0,0 +1,15 @@ +#Weapon Templates + +$Template: Base Laser + +#End + +#Primary Weapons + +$Name: Alpha + +$Name: Beta ++Use Template: Base Laser ++Use Weapon as Template: Alpha + +#End diff --git a/test/test_data/weapon/duplicate_template_warns/data/tables/test-wep.tbm b/test/test_data/weapon/duplicate_template_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..29667d2c278 --- /dev/null +++ b/test/test_data/weapon/duplicate_template_warns/data/tables/test-wep.tbm @@ -0,0 +1,7 @@ +#Weapon Templates + +$Template: Twin + +$Template: Twin + +#End diff --git a/test/test_data/weapon/missing_template_warns/data/tables/test-wep.tbm b/test/test_data/weapon/missing_template_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..2b914a5b991 --- /dev/null +++ b/test/test_data/weapon/missing_template_warns/data/tables/test-wep.tbm @@ -0,0 +1,6 @@ +#Primary Weapons + +$Name: Orphan Laser ++Use Template: No Such Template + +#End diff --git a/test/test_data/weapon/missing_weapon_as_template_warns/data/tables/test-wep.tbm b/test/test_data/weapon/missing_weapon_as_template_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..d72d0c976db --- /dev/null +++ b/test/test_data/weapon/missing_weapon_as_template_warns/data/tables/test-wep.tbm @@ -0,0 +1,6 @@ +#Primary Weapons + +$Name: Orphan ++Use Weapon as Template: Nope + +#End diff --git a/test/test_data/weapon/nocreate_with_template_warns/data/tables/test-wep.tbm b/test/test_data/weapon/nocreate_with_template_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..912fbd3e016 --- /dev/null +++ b/test/test_data/weapon/nocreate_with_template_warns/data/tables/test-wep.tbm @@ -0,0 +1,13 @@ +#Weapon Templates + +$Template: Base Laser + +#End + +#Primary Weapons + +$Name: Ghost ++nocreate ++Use Template: Base Laser + +#End diff --git a/test/test_data/weapon/secondary_from_untyped_template_detonates_on_expiration/data/tables/test-wep.tbm b/test/test_data/weapon/secondary_from_untyped_template_detonates_on_expiration/data/tables/test-wep.tbm new file mode 100644 index 00000000000..74a69138a49 --- /dev/null +++ b/test/test_data/weapon/secondary_from_untyped_template_detonates_on_expiration/data/tables/test-wep.tbm @@ -0,0 +1,32 @@ +#Weapon Templates + +$Template: Base Missile +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End + +#Secondary Weapons + +$Name: Derived Missile ++Use Template: Base Missile + +#End diff --git a/test/test_data/weapon/template_applies_to_new_weapon/data/tables/test-wep.tbm b/test/test_data/weapon/template_applies_to_new_weapon/data/tables/test-wep.tbm new file mode 100644 index 00000000000..2baa7f52fae --- /dev/null +++ b/test/test_data/weapon/template_applies_to_new_weapon/data/tables/test-wep.tbm @@ -0,0 +1,37 @@ +#Weapon Templates + +$Template: Base Laser ++Title: XSTR("Base", -1) +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End + +#Primary Weapons + +$Name: Derived Laser ++Use Template: Base Laser +$Damage: 9.0 + +$Name: Plain Laser ++Use Template: Base Laser + +#End diff --git a/test/test_data/weapon/template_display_name_inherited/data/tables/test-wep.tbm b/test/test_data/weapon/template_display_name_inherited/data/tables/test-wep.tbm new file mode 100644 index 00000000000..e349c77686a --- /dev/null +++ b/test/test_data/weapon/template_display_name_inherited/data/tables/test-wep.tbm @@ -0,0 +1,13 @@ +#Weapon Templates + +$Template: Named Base +$Display Name: Shown + +#End + +#Primary Weapons + +$Name: Derived#Laser ++Use Template: Named Base + +#End diff --git a/test/test_data/weapon/template_display_name_rebuilt/data/tables/test-wep.tbm b/test/test_data/weapon/template_display_name_rebuilt/data/tables/test-wep.tbm new file mode 100644 index 00000000000..18372c93c87 --- /dev/null +++ b/test/test_data/weapon/template_display_name_rebuilt/data/tables/test-wep.tbm @@ -0,0 +1,32 @@ +#Weapon Templates + +$Template: Base Laser +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End + +#Primary Weapons + +$Name: Derived#Laser ++Use Template: Base Laser + +#End diff --git a/test/test_data/weapon/template_free_flight_time_kept/data/tables/test-wep.tbm b/test/test_data/weapon/template_free_flight_time_kept/data/tables/test-wep.tbm new file mode 100644 index 00000000000..c4077d73ec8 --- /dev/null +++ b/test/test_data/weapon/template_free_flight_time_kept/data/tables/test-wep.tbm @@ -0,0 +1,18 @@ +#Weapon Templates + +$Template: Seeker Base +$Velocity: 300.0 +$Homing: YES + +Type: HEAT + +Turn Time: 1.0 + +View Cone: 180.0 +$Free Flight Time: 0.3 + +#End + +#Secondary Weapons + +$Name: Seeker Derived ++Use Template: Seeker Base + +#End diff --git a/test/test_data/weapon/template_german_display_name_not_inherited/data/tables/test-wep.tbm b/test/test_data/weapon/template_german_display_name_not_inherited/data/tables/test-wep.tbm new file mode 100644 index 00000000000..cf016823241 --- /dev/null +++ b/test/test_data/weapon/template_german_display_name_not_inherited/data/tables/test-wep.tbm @@ -0,0 +1,12 @@ +#Weapon Templates + +$Template: Base Laser + +#End + +#Primary Weapons + +$Name: Derived Laser ++Use Template: Base Laser + +#End diff --git a/test/test_data/weapon/template_hashed_name_not_inherited/data/tables/test-wep.tbm b/test/test_data/weapon/template_hashed_name_not_inherited/data/tables/test-wep.tbm new file mode 100644 index 00000000000..b25843c87ec --- /dev/null +++ b/test/test_data/weapon/template_hashed_name_not_inherited/data/tables/test-wep.tbm @@ -0,0 +1,12 @@ +#Weapon Templates + +$Template: Laser#Base + +#End + +#Primary Weapons + +$Name: Subach#Mk2 ++Use Template: Laser#Base + +#End diff --git a/test/test_data/weapon/template_laser_color_kept/data/tables/test-wep.tbm b/test/test_data/weapon/template_laser_color_kept/data/tables/test-wep.tbm new file mode 100644 index 00000000000..b4ce1007808 --- /dev/null +++ b/test/test_data/weapon/template_laser_color_kept/data/tables/test-wep.tbm @@ -0,0 +1,14 @@ +#Weapon Templates + +$Template: Tinted Base +@Laser Color: 255 0 0 +@Laser Color2: 0 0 255 + +#End + +#Primary Weapons + +$Name: Tinted Derived ++Use Template: Tinted Base + +#End diff --git a/test/test_data/weapon/template_of_template/data/tables/test-wep.tbm b/test/test_data/weapon/template_of_template/data/tables/test-wep.tbm new file mode 100644 index 00000000000..b4d481df4f5 --- /dev/null +++ b/test/test_data/weapon/template_of_template/data/tables/test-wep.tbm @@ -0,0 +1,36 @@ +#Weapon Templates + +$Template: Root +$Model File: +$Mass: 0.1 +$Velocity: 321.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +$Template: Branch ++Use Template: Root +$Damage: 3.0 + +#End + +#Primary Weapons + +$Name: Leaf ++Use Template: Branch + +#End diff --git a/test/test_data/weapon/template_on_existing_weapon_warns/data/tables/test-wep.tbm b/test/test_data/weapon/template_on_existing_weapon_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..db123a851c7 --- /dev/null +++ b/test/test_data/weapon/template_on_existing_weapon_warns/data/tables/test-wep.tbm @@ -0,0 +1,54 @@ +#Weapon Templates + +$Template: Base Laser +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End + +#Primary Weapons + +$Name: Alpha +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +$Name: Alpha ++Use Template: Base Laser + +#End diff --git a/test/test_data/weapon/template_proximity_lists_follow_name/data/tables/test-wep.tbm b/test/test_data/weapon/template_proximity_lists_follow_name/data/tables/test-wep.tbm new file mode 100644 index 00000000000..a59fbfbdda9 --- /dev/null +++ b/test/test_data/weapon/template_proximity_lists_follow_name/data/tables/test-wep.tbm @@ -0,0 +1,14 @@ +#Weapon Templates + +$Template: Flak Base +$Proximity Radius: 50 + +Proximity Type: ( "fighter" ) + +#End + +#Secondary Weapons + +$Name: Flak Derived ++Use Template: Flak Base + +#End diff --git a/test/test_data/weapon/template_same_name_as_weapon_keeps_ssm/data/tables/test-wep.tbm b/test/test_data/weapon/template_same_name_as_weapon_keeps_ssm/data/tables/test-wep.tbm new file mode 100644 index 00000000000..5562c34e970 --- /dev/null +++ b/test/test_data/weapon/template_same_name_as_weapon_keeps_ssm/data/tables/test-wep.tbm @@ -0,0 +1,12 @@ +#Weapon Templates + +$Template: Trebuchet + +#End + +#Secondary Weapons + +$Name: Trebuchet +$SSM: Trebuchet Strike + +#End diff --git a/test/test_data/weapon/template_self_reference_warns/data/tables/test-wep.tbm b/test/test_data/weapon/template_self_reference_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..c3a02bb89a5 --- /dev/null +++ b/test/test_data/weapon/template_self_reference_warns/data/tables/test-wep.tbm @@ -0,0 +1,26 @@ +#Weapon Templates + +$Template: Ouroboros ++Use Template: Ouroboros +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End diff --git a/test/test_data/weapon/template_ssm_copied/data/tables/test-wep.tbm b/test/test_data/weapon/template_ssm_copied/data/tables/test-wep.tbm new file mode 100644 index 00000000000..ce5ac9ee399 --- /dev/null +++ b/test/test_data/weapon/template_ssm_copied/data/tables/test-wep.tbm @@ -0,0 +1,13 @@ +#Weapon Templates + +$Template: Artillery Base +$SSM: Some Strike + +#End + +#Secondary Weapons + +$Name: Artillery Derived ++Use Template: Artillery Base + +#End diff --git a/test/test_data/weapon/template_ssm_numeric_override/data/tables/test-wep.tbm b/test/test_data/weapon/template_ssm_numeric_override/data/tables/test-wep.tbm new file mode 100644 index 00000000000..02158724cc6 --- /dev/null +++ b/test/test_data/weapon/template_ssm_numeric_override/data/tables/test-wep.tbm @@ -0,0 +1,14 @@ +#Weapon Templates + +$Template: Artillery Base +$SSM: Some Strike + +#End + +#Secondary Weapons + +$Name: Artillery Numeric ++Use Template: Artillery Base +$SSM: 0 + +#End diff --git a/test/test_data/weapon/template_substitution_follows_name/data/tables/test-wep.tbm b/test/test_data/weapon/template_substitution_follows_name/data/tables/test-wep.tbm new file mode 100644 index 00000000000..ee14a4892ca --- /dev/null +++ b/test/test_data/weapon/template_substitution_follows_name/data/tables/test-wep.tbm @@ -0,0 +1,16 @@ +#Weapon Templates + +$Template: Sub Base +$substitute: Sub Other + +period: 2 + +#End + +#Primary Weapons + +$Name: Sub Other + +$Name: Sub Derived ++Use Template: Sub Base + +#End diff --git a/test/test_data/weapon/template_with_nocreate_warns/data/tables/test-wep.tbm b/test/test_data/weapon/template_with_nocreate_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..b57a1ee9d0e --- /dev/null +++ b/test/test_data/weapon/template_with_nocreate_warns/data/tables/test-wep.tbm @@ -0,0 +1,26 @@ +#Weapon Templates + +$Template: Base Laser ++nocreate +$Model File: +$Mass: 0.1 +$Velocity: 500.0 +$Fire Wait: 0.5 +$Damage: 7.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +#End diff --git a/test/test_data/weapon/template_with_remove_warns/data/tables/test-wep.tbm b/test/test_data/weapon/template_with_remove_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..3e0498ffd02 --- /dev/null +++ b/test/test_data/weapon/template_with_remove_warns/data/tables/test-wep.tbm @@ -0,0 +1,12 @@ +#Weapon Templates + +$Template: Doomed ++remove + +#End + +#Primary Weapons + +$Name: Doomed + +#End diff --git a/test/test_data/weapon/template_with_weapon_as_template_warns/data/tables/test-wep.tbm b/test/test_data/weapon/template_with_weapon_as_template_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..0b0ba5e96f9 --- /dev/null +++ b/test/test_data/weapon/template_with_weapon_as_template_warns/data/tables/test-wep.tbm @@ -0,0 +1,13 @@ +#Weapon Templates + +$Template: Base Laser ++Use Weapon as Template: Subach HL-7 +$Damage: 3.0 + +#End + +#Primary Weapons + +$Name: Survivor + +#End diff --git a/test/test_data/weapon/untyped_template_ballistic_rearm_warns/data/tables/test-wep.tbm b/test/test_data/weapon/untyped_template_ballistic_rearm_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..f3c23489dba --- /dev/null +++ b/test/test_data/weapon/untyped_template_ballistic_rearm_warns/data/tables/test-wep.tbm @@ -0,0 +1,13 @@ +#Weapon Templates + +$Template: Gun Base +$Flags: ( "ballistic" "player allowed" ) + +#End + +#Primary Weapons + +$Name: Gun Derived ++Use Template: Gun Base + +#End diff --git a/test/test_data/weapon/untyped_template_free_flight_warns/data/tables/test-wep.tbm b/test/test_data/weapon/untyped_template_free_flight_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..6205b930393 --- /dev/null +++ b/test/test_data/weapon/untyped_template_free_flight_warns/data/tables/test-wep.tbm @@ -0,0 +1,24 @@ +#Weapon Templates + +$Template: Seeker Base +$Velocity: 300.0 +$Homing: YES + +Type: HEAT + +Turn Time: 1.0 + +View Cone: 180.0 + +#End + +#Primary Weapons + +$Name: Seeker Primary ++Use Template: Seeker Base + +#End + +#Secondary Weapons + +$Name: Seeker Secondary ++Use Template: Seeker Base + +#End diff --git a/test/test_data/weapon/untyped_template_keeps_ballistic/data/tables/test-wep.tbm b/test/test_data/weapon/untyped_template_keeps_ballistic/data/tables/test-wep.tbm new file mode 100644 index 00000000000..2fbcddd015f --- /dev/null +++ b/test/test_data/weapon/untyped_template_keeps_ballistic/data/tables/test-wep.tbm @@ -0,0 +1,14 @@ +#Weapon Templates + +$Template: Gun Base +$Rearm Rate: 50.0 +$Flags: ( "ballistic" "player allowed" ) + +#End + +#Primary Weapons + +$Name: Gun Derived ++Use Template: Gun Base + +#End diff --git a/test/test_data/weapon/untyped_template_reloaded_per_batch_by_section/data/tables/test-wep.tbm b/test/test_data/weapon/untyped_template_reloaded_per_batch_by_section/data/tables/test-wep.tbm new file mode 100644 index 00000000000..4905fcbf14b --- /dev/null +++ b/test/test_data/weapon/untyped_template_reloaded_per_batch_by_section/data/tables/test-wep.tbm @@ -0,0 +1,20 @@ +#Weapon Templates + +$Template: Any Base +$Velocity: 100.0 + +#End + +#Primary Weapons + +$Name: Any Primary ++Use Template: Any Base + +#End + +#Secondary Weapons + +$Name: Any Secondary ++Use Template: Any Base + +#End diff --git a/test/test_data/weapon/weapon_as_template/data/tables/test-wep.tbm b/test/test_data/weapon/weapon_as_template/data/tables/test-wep.tbm new file mode 100644 index 00000000000..264b830f928 --- /dev/null +++ b/test/test_data/weapon/weapon_as_template/data/tables/test-wep.tbm @@ -0,0 +1,29 @@ +#Primary Weapons + +$Name: Alpha +$Model File: +$Mass: 0.1 +$Velocity: 640.0 +$Fire Wait: 0.5 +$Damage: 4.0 +$Blast Force: 1.0 +$Inner Radius: 1.0 +$Outer Radius: 1.5 +$Shockwave Speed: 0 +$Armor Factor: 1.0 +$Shield Factor: 1.0 +$Subsystem Factor: 1.0 +$Lifetime: 2.0 +$Energy Consumed: 0.1 +$Cargo Size: 1.0 +$Homing: NO +$LaunchSnd: -1 +$ImpactSnd: -1 +$FlyBySnd: -1 +$Flags: ( "player allowed" ) + +$Name: Beta ++Use Weapon as Template: Alpha +$Damage: 2.0 + +#End diff --git a/test/test_data/weapon/weapon_as_template_self_warns/data/tables/test-wep.tbm b/test/test_data/weapon/weapon_as_template_self_warns/data/tables/test-wep.tbm new file mode 100644 index 00000000000..0e7c736c35d --- /dev/null +++ b/test/test_data/weapon/weapon_as_template_self_warns/data/tables/test-wep.tbm @@ -0,0 +1,6 @@ +#Primary Weapons + +$Name: Foo ++Use Weapon as Template: Foo + +#End From ba7f9358e9e66d4b9b6e3b5ba38fbb6e2b391373 Mon Sep 17 00:00:00 2001 From: Dustinspace217 Date: Thu, 15 Oct 2026 07:02:33 -0700 Subject: [PATCH 4/4] Fix a stale comment on the proximity pending lists --- code/weapon/weapons.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index f5b1ffa2960..851e3947885 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -88,7 +88,8 @@ SCP_unordered_map Delayed_SSM_indices_data; SCP_vector Delayed_SSM_indices; // Temporary storage for proximity type/class names pending resolution after ship_init(). -// Keyed by weapon_info_index (not name) so a duplicate-named entry can't clobber another's pending list. +// Keyed by the parse key (the name, prefixed with $Template: for templates) so a duplicate-named entry can't clobber +// another's pending list. static SCP_unordered_map> Pending_proximity_type_names; static SCP_unordered_map> Pending_proximity_class_names;