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/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..851e3947885 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" @@ -87,9 +88,10 @@ 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. -static SCP_unordered_map> Pending_proximity_type_names; -static SCP_unordered_map> Pending_proximity_class_names; +// 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; #ifndef NDEBUG @@ -101,6 +103,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 @@ -381,6 +384,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) { @@ -499,7 +513,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); } } @@ -644,9 +658,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); @@ -766,18 +826,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 @@ -788,81 +856,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, '#'); @@ -898,19 +1071,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); @@ -1374,14 +1552,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. @@ -1951,13 +2127,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:")) { @@ -2099,7 +2277,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 { @@ -2181,6 +2359,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 @@ -2194,7 +2376,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]) @@ -2214,7 +2396,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) { @@ -2653,7 +2835,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); @@ -3869,7 +4051,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; @@ -3972,7 +4154,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; @@ -4003,17 +4185,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 @@ -4207,7 +4393,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}; @@ -4216,14 +4402,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); } @@ -4394,11 +4584,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; } } @@ -4409,7 +4609,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; } } @@ -4420,7 +4620,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; } } @@ -4431,7 +4631,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; @@ -5106,16 +5306,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()); @@ -5127,8 +5333,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()); @@ -5154,6 +5363,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); @@ -5191,6 +5401,10 @@ void weapon_close() } Spawn_names.clear(); + Pending_proximity_type_names.clear(); + Pending_proximity_class_names.clear(); + + Weapons_inited = false; } /** @@ -10158,6 +10372,336 @@ 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..98bb783abc2 100644 --- a/test/src/weapon/weapons.cpp +++ b/test/src/weapon/weapons.cpp @@ -4,8 +4,23 @@ #include "util/FSTestFixture.h" #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) { @@ -18,6 +33,7 @@ class WeaponsParseTest : public test::FSTestFixture { } void TearDown() override { weapon_close(); + Weapon_info.clear(); test::FSTestFixture::TearDown(); } @@ -34,3 +50,322 @@ 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"); + 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); +} 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