mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 07:28:22 -10:00
* fix ship duplication * prop and jump node duplication * make sure objects on layers are properly registered * waypoint duplication * some cleanup * address feedback * assertion for clang
317 lines
12 KiB
C++
317 lines
12 KiB
C++
// Helpers for editor-side object duplication (Ctrl-drag, Clone Marked Objects).
|
|
// Shared between FRED and QtFRED.
|
|
|
|
#include "objectduplication.h"
|
|
|
|
#include "ai/ai.h"
|
|
#include "globalincs/linklist.h"
|
|
#include "jumpnode/jumpnode.h"
|
|
#include "mission/missionparse.h"
|
|
#include "model/model.h"
|
|
#include "object/object.h"
|
|
#include "object/waypoint.h"
|
|
#include "parse/sexp.h"
|
|
#include "prop/prop.h"
|
|
#include "ship/ship.h"
|
|
|
|
// FRED/QtFRED each own their own copy of these arrays (defined in
|
|
// fred2/management.cpp and qtfred/src/mission/Editor.cpp)
|
|
extern char Fred_alt_names[MAX_SHIPS][NAME_LENGTH + 1];
|
|
extern char Fred_callsigns[MAX_SHIPS][NAME_LENGTH + 1];
|
|
|
|
// Per-ship field handling here MUST stay in sync with:
|
|
// Fred_mission_save::save_objects() / save_common_object_data() in missionsave.cpp
|
|
// parse_create_object_sub() in missionparse.cpp
|
|
// Field order below mirrors save_objects() so the two can be diffed.
|
|
void clone_ship_instance_data(int src_shipnum, int dest_shipnum)
|
|
{
|
|
Assertion(src_shipnum >= 0 && src_shipnum < MAX_SHIPS, "clone_ship_instance_data: bad src %d", src_shipnum);
|
|
Assertion(dest_shipnum >= 0 && dest_shipnum < MAX_SHIPS, "clone_ship_instance_data: bad dest %d", dest_shipnum);
|
|
Assertion(src_shipnum != dest_shipnum, "clone_ship_instance_data: src and dest are the same ship");
|
|
|
|
ship* src = &Ships[src_shipnum];
|
|
ship* dest = &Ships[dest_shipnum];
|
|
Assertion(src->ship_info_index == dest->ship_info_index, "clone_ship_instance_data: dest must be the same ship class as src");
|
|
|
|
object* src_obj = &Objects[src->objnum];
|
|
object* dest_obj = &Objects[dest->objnum];
|
|
ai_info* src_ai = &Ai_info[src->ai_index];
|
|
ai_info* dest_ai = &Ai_info[dest->ai_index];
|
|
|
|
// display name (the Has_display_name flag is NOT in Parse_ship_flags, so the
|
|
// flag-copy loop below doesn't carry it - mirror it here alongside the string).
|
|
dest->display_name = src->display_name;
|
|
if (src->flags[Ship::Ship_Flags::Has_display_name]) {
|
|
dest->flags.set(Ship::Ship_Flags::Has_display_name);
|
|
} else {
|
|
dest->flags.remove(Ship::Ship_Flags::Has_display_name);
|
|
}
|
|
|
|
// alt classes
|
|
dest->s_alt_classes = src->s_alt_classes;
|
|
|
|
// alt name / callsign
|
|
strcpy_s(Fred_alt_names[dest_shipnum], Fred_alt_names[src_shipnum]);
|
|
strcpy_s(Fred_callsigns[dest_shipnum], Fred_callsigns[src_shipnum]);
|
|
dest->alt_type_index = src->alt_type_index;
|
|
dest->callsign_index = src->callsign_index;
|
|
|
|
// team / team color
|
|
dest->team = src->team;
|
|
dest->team_name = src->team_name;
|
|
dest->ship_iff_color = src->ship_iff_color;
|
|
|
|
// AI class (lives both on the AI and mirrored on the ship's weapons struct;
|
|
// the weapons.ai_class part is included in the full weapons-struct copy below)
|
|
dest_ai->ai_class = src_ai->ai_class;
|
|
|
|
// AI goals
|
|
for (int i = 0; i < MAX_AI_GOALS; i++) {
|
|
dest_ai->goals[i] = src_ai->goals[i];
|
|
}
|
|
|
|
// cargo
|
|
dest->cargo1 = src->cargo1;
|
|
strcpy_s(dest->cargo_title, src->cargo_title);
|
|
|
|
// ===== begin save_common_object_data mirror =====
|
|
|
|
// initial velocity / hull / shields
|
|
dest_obj->phys_info.speed = src_obj->phys_info.speed;
|
|
dest_obj->phys_info.fspeed = src_obj->phys_info.fspeed;
|
|
dest_obj->hull_strength = src_obj->hull_strength;
|
|
dest_obj->shield_quadrant = src_obj->shield_quadrant;
|
|
|
|
// weapons (banks, ammo, ai_class... full struct, the assignment is a value copy)
|
|
dest->weapons = src->weapons;
|
|
|
|
// subsystems: walk the lists pairwise. current_hits and max_hits both
|
|
// matter so that special-hitpoint scaling carries over; subsystem cargo
|
|
// and per-turret weapon loadout / ai_class are also editor-editable.
|
|
{
|
|
ship_subsys* sp_src = GET_FIRST(&src->subsys_list);
|
|
ship_subsys* sp_dest = GET_FIRST(&dest->subsys_list);
|
|
while (sp_dest != END_OF_LIST(&dest->subsys_list) && sp_src != END_OF_LIST(&src->subsys_list)) {
|
|
sp_dest->current_hits = sp_src->current_hits;
|
|
sp_dest->max_hits = sp_src->max_hits;
|
|
sp_dest->subsys_cargo_name = sp_src->subsys_cargo_name;
|
|
strcpy_s(sp_dest->subsys_cargo_title, sp_src->subsys_cargo_title);
|
|
|
|
if (sp_dest->system_info && sp_dest->system_info->type == SUBSYSTEM_TURRET) {
|
|
sp_dest->weapons = sp_src->weapons;
|
|
}
|
|
|
|
sp_src = GET_NEXT(sp_src);
|
|
sp_dest = GET_NEXT(sp_dest);
|
|
}
|
|
}
|
|
|
|
// ===== end save_common_object_data mirror =====
|
|
|
|
// arrival / departure - skip entirely for player starts: create_player has
|
|
// already locked arrival_cue to Locked_sexp_true and departure_cue to
|
|
// Locked_sexp_false, and player starts ignore the other arrival/departure
|
|
// fields. Overwriting the cues here would break the locked-cue invariant.
|
|
if (!dest_obj->flags[Object::Object_Flags::Player_ship]) {
|
|
dest->arrival_location = src->arrival_location;
|
|
dest->arrival_distance = src->arrival_distance;
|
|
dest->arrival_anchor = src->arrival_anchor;
|
|
dest->arrival_path_mask = src->arrival_path_mask;
|
|
dest->arrival_delay = src->arrival_delay;
|
|
dest->arrival_cue = dup_sexp_chain(src->arrival_cue);
|
|
|
|
dest->departure_location = src->departure_location;
|
|
dest->departure_anchor = src->departure_anchor;
|
|
dest->departure_path_mask = src->departure_path_mask;
|
|
dest->departure_delay = src->departure_delay;
|
|
dest->departure_cue = dup_sexp_chain(src->departure_cue);
|
|
}
|
|
|
|
// warp params
|
|
dest->warpin_params_index = src->warpin_params_index;
|
|
dest->warpout_params_index = src->warpout_params_index;
|
|
|
|
// ship flags - driven by Parse_ship_flags so duplication stays in sync with
|
|
// parse/save automatically when new editor-editable ship flags are added.
|
|
// Runtime/transient flags (Dying, Depart_warp, From_player_wing, ...) and
|
|
// instance-coupled flags (Dock_leader) are not in Parse_ship_flags and so
|
|
// are correctly NOT copied here.
|
|
for (size_t i = 0; i < Num_Parse_ship_flags; ++i) {
|
|
const auto flag = Parse_ship_flags[i].def;
|
|
if (src->flags[flag]) {
|
|
dest->flags.set(flag);
|
|
} else {
|
|
dest->flags.remove(flag);
|
|
}
|
|
}
|
|
|
|
// object flags - driven by Parse_ship_object_flags. Skip Player_ship: that
|
|
// bit is managed by create_player/create_ship at the caller and copying it
|
|
// would either redundantly re-set it (player-start -> player-start) or
|
|
// wrongly promote a regular-ship dup to a player ship (demoted dup case).
|
|
for (size_t i = 0; i < Num_Parse_ship_object_flags; ++i) {
|
|
const auto flag = Parse_ship_object_flags[i].def;
|
|
if (flag == Object::Object_Flags::Player_ship) {
|
|
continue;
|
|
}
|
|
if (src_obj->flags[flag]) {
|
|
dest_obj->flags.set(flag);
|
|
} else {
|
|
dest_obj->flags.remove(flag);
|
|
}
|
|
}
|
|
|
|
// AI flags - driven by Parse_ship_ai_flags so duplication stays in sync with
|
|
// parse/save automatically when new editor-editable AI flags are added.
|
|
for (size_t i = 0; i < Num_Parse_ship_ai_flags; ++i) {
|
|
const auto flag = Parse_ship_ai_flags[i].def;
|
|
if (src_ai->ai_flags[flag]) {
|
|
dest_ai->ai_flags.set(flag);
|
|
} else {
|
|
dest_ai->ai_flags.remove(flag);
|
|
}
|
|
}
|
|
// kamikaze_damage is editor-editable independent of the Kamikaze flag (see
|
|
// ShipFlagsDialogModel), so copy unconditionally.
|
|
dest_ai->kamikaze_damage = src_ai->kamikaze_damage;
|
|
|
|
// respawn / escort / guardian
|
|
dest->respawn_priority = src->respawn_priority;
|
|
dest->escort_priority = src->escort_priority;
|
|
dest->ship_guardian_threshold = src->ship_guardian_threshold;
|
|
|
|
// special explosion
|
|
dest->use_special_explosion = src->use_special_explosion;
|
|
dest->special_exp_damage = src->special_exp_damage;
|
|
dest->special_exp_blast = src->special_exp_blast;
|
|
dest->special_exp_inner = src->special_exp_inner;
|
|
dest->special_exp_outer = src->special_exp_outer;
|
|
dest->use_shockwave = src->use_shockwave;
|
|
dest->special_exp_shockwave_speed = src->special_exp_shockwave_speed;
|
|
dest->special_exp_deathroll_time = src->special_exp_deathroll_time;
|
|
|
|
// special hitpoints / shield (and the derived max strengths the parse
|
|
// path sets at lines 2321-2326 of missionparse.cpp)
|
|
dest->special_hitpoints = src->special_hitpoints;
|
|
dest->special_shield = src->special_shield;
|
|
dest->ship_max_hull_strength = src->ship_max_hull_strength;
|
|
dest->ship_max_shield_strength = src->ship_max_shield_strength;
|
|
dest->max_shield_recharge = src->max_shield_recharge;
|
|
if (dest->special_hitpoints > 0) {
|
|
ship_recalc_subsys_strength(dest);
|
|
}
|
|
|
|
// hotkey
|
|
dest->hotkey = src->hotkey;
|
|
|
|
// destroy-at (Kill_before_mission flag already carried via shipp->flags)
|
|
dest->final_death_time = src->final_death_time;
|
|
|
|
// orders
|
|
dest->orders_accepted = src->orders_accepted;
|
|
|
|
// group / layer
|
|
dest->group = src->group;
|
|
dest->fred_layer = src->fred_layer;
|
|
|
|
// scoring / persona
|
|
dest->score = src->score;
|
|
dest->assist_score_pct = src->assist_score_pct;
|
|
dest->persona_index = src->persona_index;
|
|
|
|
// texture replacement... copy any entries whose ship_name matches src.
|
|
// Skip from_table entries: those came from the ship class's .tbl/.tbm and
|
|
// apply to every instance of the class automatically, so re-attaching one
|
|
// to the duplicate would create a duplicate override that save_objects
|
|
// wouldn't write anyway.
|
|
{
|
|
SCP_vector<texture_replace> new_entries;
|
|
for (const auto& tr : Fred_texture_replacements) {
|
|
if (!stricmp(tr.ship_name, src->ship_name) && !(tr.from_table)) {
|
|
texture_replace copy = tr;
|
|
strcpy_s(copy.ship_name, dest->ship_name);
|
|
new_entries.push_back(copy);
|
|
}
|
|
}
|
|
for (auto& entry : new_entries) {
|
|
Fred_texture_replacements.push_back(std::move(entry));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Per-prop field handling here MUST stay in sync with:
|
|
// Fred_mission_save::save_props() in missionsave.cpp
|
|
// parse_prop() in missionparse.cpp
|
|
void clone_prop_instance_data(int src_prop_id, int dest_prop_id)
|
|
{
|
|
Assertion(src_prop_id != dest_prop_id, "clone_prop_instance_data: src and dest are the same prop");
|
|
|
|
prop* src = prop_id_lookup(src_prop_id);
|
|
prop* dest = prop_id_lookup(dest_prop_id);
|
|
Assertion(src && dest, "clone_prop_instance_data: invalid src or dest prop id");
|
|
Assertion(src->prop_info_index == dest->prop_info_index, "clone_prop_instance_data: dest must be the same prop class as src");
|
|
|
|
// fred view layer
|
|
dest->fred_layer = src->fred_layer;
|
|
|
|
// object flag the prop editor exposes (see save_props)
|
|
if (Objects[src->objnum].flags[Object::Object_Flags::Collides]) {
|
|
Objects[dest->objnum].flags.set(Object::Object_Flags::Collides);
|
|
} else {
|
|
Objects[dest->objnum].flags.remove(Object::Object_Flags::Collides);
|
|
}
|
|
}
|
|
|
|
// Per-jump-node field handling here MUST stay in sync with:
|
|
// Fred_mission_save::save_waypoints() jump-node section in missionsave.cpp
|
|
// the "$Jump Node:" parse block in missionparse.cpp
|
|
void clone_jump_node_instance_data(const CJumpNode& src, CJumpNode& dest)
|
|
{
|
|
// display name (the JN_HAS_DISPLAY_NAME flag is set by SetDisplayName)
|
|
if (src.HasDisplayName()) {
|
|
dest.SetDisplayName(src.GetDisplayName());
|
|
}
|
|
|
|
// special model - SetModel also sets m_radius and the JN_SPECIAL_MODEL flag.
|
|
// Parse uses default show_polys=false; JN_SHOW_POLYS isn't persisted, so we
|
|
// don't carry it across either.
|
|
if (src.IsSpecialModel()) {
|
|
polymodel* pm = model_get(src.GetModelNumber());
|
|
if (pm) {
|
|
dest.SetModel(pm->filename);
|
|
}
|
|
}
|
|
|
|
// color (SetAlphaColor manages the JN_USE_DISPLAY_COLOR flag)
|
|
if (src.IsColored()) {
|
|
const color& c = src.GetColor();
|
|
dest.SetAlphaColor(c.red, c.green, c.blue, c.alpha);
|
|
}
|
|
|
|
// hidden state
|
|
dest.SetVisibility(!src.IsHidden());
|
|
|
|
// fred view layer
|
|
dest.SetFredLayer(src.GetFredLayer());
|
|
}
|
|
|
|
// Per-path field handling here MUST stay in sync with:
|
|
// Fred_mission_save::save_waypoints() (waypoint-list section) in missionsave.cpp
|
|
// parse_waypoint_list() in missionparse.cpp
|
|
void clone_waypoint_path_instance_data(int src_list_index, int dest_list_index)
|
|
{
|
|
waypoint_list* src = find_waypoint_list_at_index(src_list_index);
|
|
waypoint_list* dest = find_waypoint_list_at_index(dest_list_index);
|
|
if (src == nullptr || dest == nullptr || src == dest) {
|
|
return;
|
|
}
|
|
|
|
dest->set_no_draw_lines(src->get_no_draw_lines());
|
|
if (src->get_has_custom_color()) {
|
|
dest->set_color(src->get_color_r(), src->get_color_g(), src->get_color_b());
|
|
} else {
|
|
dest->clear_color();
|
|
}
|
|
dest->set_fred_layer(src->get_fred_layer());
|
|
}
|