mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
Add generate_ship_usage_list_team alongside the weaponry twin that already existed in missioneditor/common, so both loadout editors can drop their TVT/non-TVT branches and simply iterate Num_teams. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
755 lines
24 KiB
C++
755 lines
24 KiB
C++
// methods and members common to any mission editor FSO may have
|
|
#include "common.h"
|
|
#include "ai/ai.h"
|
|
#include "cmdline/cmdline.h"
|
|
#include "controlconfig/controlsconfig.h"
|
|
#include "globalincs/linklist.h"
|
|
#include "mission/missionparse.h"
|
|
#include "iff_defs/iff_defs.h"
|
|
#include "io/cursor.h"
|
|
#include "jumpnode/jumpnode.h"
|
|
#include "localization/fhash.h"
|
|
#include "mission/missionbriefcommon.h"
|
|
#include "mission/missioncampaign.h"
|
|
#include "model/model.h"
|
|
#include "object/object.h"
|
|
#include "object/waypoint.h"
|
|
#include "parse/sexp.h"
|
|
#include "prop/prop.h"
|
|
#include "render/batching.h"
|
|
#include "ship/ship.h"
|
|
#include "weapon/weapon.h"
|
|
|
|
#include <algorithm>
|
|
|
|
// to keep track of data
|
|
char Voice_abbrev_briefing[NAME_LENGTH];
|
|
char Voice_abbrev_campaign[NAME_LENGTH];
|
|
char Voice_abbrev_command_briefing[NAME_LENGTH];
|
|
char Voice_abbrev_debriefing[NAME_LENGTH];
|
|
char Voice_abbrev_message[NAME_LENGTH];
|
|
char Voice_abbrev_mission[NAME_LENGTH];
|
|
bool Voice_no_replace_filenames;
|
|
char Voice_script_entry_format[NOTES_LENGTH];
|
|
int Voice_export_selection; // 0=everything, 1=cmd brief, 2=brief, 3=debrief, 4=messages
|
|
bool Voice_group_messages;
|
|
|
|
SCP_string Voice_script_default_string = "Sender: $sender\r\nPersona: $persona\r\nFile: $filename\r\nMessage: $message";
|
|
SCP_string Voice_script_instructions_string = "$name - name of the message\r\n"
|
|
"$filename - name of the message file\r\n"
|
|
"$message - text of the message\r\n"
|
|
"$persona - persona of the sender\r\n"
|
|
"$sender - name of the sender\r\n"
|
|
"$note - message notes\r\n\r\n"
|
|
"Note that $persona and $sender will only appear for the Message section.";
|
|
|
|
float normalize_degrees(float deg)
|
|
{
|
|
while (deg < -180.0f)
|
|
deg += 360.0f;
|
|
while (deg > 180.0f)
|
|
deg -= 360.0f;
|
|
// check for negative zero
|
|
if (deg == -0.0f)
|
|
deg = 0.0f;
|
|
return deg;
|
|
}
|
|
|
|
void time_to_mission_info_string(const std::tm* src, char* dest, size_t dest_max_len)
|
|
{
|
|
std::strftime(dest, dest_max_len, "%x at %X", src);
|
|
}
|
|
|
|
void stuff_special_arrival_anchor_name(char* buf, int iff_index, int restrict_to_players, bool retail_format)
|
|
{
|
|
const char* iff_name = Iff_info[iff_index].iff_name;
|
|
|
|
// stupid retail hack
|
|
if (retail_format && !stricmp(iff_name, "hostile") && !restrict_to_players)
|
|
iff_name = "enemy";
|
|
|
|
if (restrict_to_players)
|
|
sprintf(buf, "<any %s player>", iff_name);
|
|
else
|
|
sprintf(buf, "<any %s>", iff_name);
|
|
|
|
strlwr(buf);
|
|
}
|
|
|
|
void stuff_special_arrival_anchor_name(char* buf, int anchor_num, bool retail_format)
|
|
{
|
|
// filter out iff
|
|
int iff_index = anchor_num;
|
|
iff_index &= ~ANCHOR_SPECIAL_ARRIVAL;
|
|
iff_index &= ~ANCHOR_SPECIAL_ARRIVAL_PLAYER;
|
|
|
|
// filter players
|
|
int restrict_to_players = (anchor_num & ANCHOR_SPECIAL_ARRIVAL_PLAYER);
|
|
|
|
// get name
|
|
stuff_special_arrival_anchor_name(buf, iff_index, restrict_to_players, retail_format);
|
|
}
|
|
|
|
// Ship and wing arrival and departure anchors should always be ship registry entry indexes, except for a very brief window during mission parsing.
|
|
// But FRED and QtFRED dialogs use ship indexes instead. So, rather than refactor all the dialogs, this converts between the two. If an anchor
|
|
// is a valid ship registry index, the equivalent ship index is returned; otherwise the special value (-1 or a flag) is returned instead.
|
|
int anchor_to_target(anchor_t anchor)
|
|
{
|
|
auto anchor_entry = ship_registry_get(anchor);
|
|
return anchor_entry ? anchor_entry->shipnum : anchor.value();
|
|
}
|
|
|
|
// Ship and wing arrival and departure anchors should always be ship registry entry indexes, except for a very brief window during mission parsing.
|
|
// But FRED and QtFRED dialogs use ship indexes instead. So, rather than refactor all the dialogs, this converts between the two. If a target
|
|
// is a valid ship index, the equivalent ship registry index is returned; otherwise the special value (-1 or a flag) is returned instead.
|
|
anchor_t target_to_anchor(int target)
|
|
{
|
|
if (target >= 0 && target < MAX_SHIPS)
|
|
return anchor_t(ship_registry_get_index(Ships[target].ship_name));
|
|
else
|
|
return anchor_t(target);
|
|
}
|
|
|
|
void update_custom_wing_indexes()
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < MAX_STARTING_WINGS; i++)
|
|
Starting_wings[i] = wing_name_lookup(Starting_wing_names[i], 1);
|
|
|
|
for (i = 0; i < MAX_SQUADRON_WINGS; i++)
|
|
Squadron_wings[i] = wing_name_lookup(Squadron_wing_names[i], 1);
|
|
|
|
for (i = 0; i < MAX_TVT_WINGS; i++)
|
|
TVT_wings[i] = wing_name_lookup(TVT_wing_names[i], 1);
|
|
}
|
|
|
|
void generate_ship_usage_list_team(int team, SCP_map<int, int>& usage)
|
|
{
|
|
usage.clear();
|
|
|
|
if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS)
|
|
{
|
|
Assert(team >= 0 && team < MAX_TVT_TEAMS);
|
|
|
|
for (int i = 0; i < MAX_TVT_WINGS_PER_TEAM; i++)
|
|
generate_ship_usage_list_wing(TVT_wings[(team * MAX_TVT_WINGS_PER_TEAM) + i], usage);
|
|
}
|
|
else
|
|
{
|
|
for (int starting_wing: Starting_wings)
|
|
generate_ship_usage_list_wing(starting_wing, usage);
|
|
}
|
|
}
|
|
|
|
void generate_ship_usage_list_wing(int wing_num, SCP_map<int, int>& usage)
|
|
{
|
|
int i;
|
|
|
|
if (wing_num < 0) {
|
|
return;
|
|
}
|
|
|
|
i = Wings[wing_num].wave_count;
|
|
while (i--) {
|
|
usage[Ships[Wings[wing_num].ship_index[i]].ship_info_index]++;
|
|
}
|
|
}
|
|
|
|
void generate_weaponry_usage_list_team(int team, SCP_map<int, int>& usage)
|
|
{
|
|
int i;
|
|
|
|
usage.clear();
|
|
|
|
if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) {
|
|
Assert(team >= 0 && team < MAX_TVT_TEAMS);
|
|
|
|
for (i = 0; i < MAX_TVT_WINGS_PER_TEAM; i++) {
|
|
generate_weaponry_usage_list_wing(TVT_wings[(team * MAX_TVT_WINGS_PER_TEAM) + i], usage);
|
|
}
|
|
} else {
|
|
for (i = 0; i < MAX_STARTING_WINGS; i++) {
|
|
generate_weaponry_usage_list_wing(Starting_wings[i], usage);
|
|
}
|
|
}
|
|
}
|
|
|
|
void generate_weaponry_usage_list_wing(int wing_num, SCP_map<int, int>& usage)
|
|
{
|
|
int i, j;
|
|
ship_weapon* swp;
|
|
|
|
if (wing_num < 0) {
|
|
return;
|
|
}
|
|
|
|
i = Wings[wing_num].wave_count;
|
|
while (i--) {
|
|
swp = &Ships[Wings[wing_num].ship_index[i]].weapons;
|
|
j = swp->num_primary_banks;
|
|
while (j--) {
|
|
if (Weapon_info.in_bounds(swp->primary_bank_weapons[j])) {
|
|
usage[swp->primary_bank_weapons[j]]++;
|
|
}
|
|
}
|
|
|
|
j = swp->num_secondary_banks;
|
|
while (j--) {
|
|
if (Weapon_info.in_bounds(swp->secondary_bank_weapons[j])) {
|
|
usage[swp->secondary_bank_weapons[j]] +=
|
|
sz2i(floor((swp->secondary_bank_ammo[j] * swp->secondary_bank_capacity[j] / 100.0f /
|
|
Weapon_info[swp->secondary_bank_weapons[j]].cargo_size) + 0.5f));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ensure_valid_player_start_shipnum()
|
|
{
|
|
// nothing to do if the current player start is still valid
|
|
if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS
|
|
&& Ships[Player_start_shipnum].objnum >= 0
|
|
&& Objects[Ships[Player_start_shipnum].objnum].type == OBJ_START)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// otherwise repoint to the first remaining player start, or -1 if there are none
|
|
Player_start_shipnum = -1;
|
|
for (auto *objp : list_range(&obj_used_list))
|
|
{
|
|
if (objp->type == OBJ_START)
|
|
{
|
|
Player_start_shipnum = objp->instance;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool set_single_player_start(int objnum)
|
|
{
|
|
if (objnum < 0 || objnum >= MAX_OBJECTS
|
|
|| (Objects[objnum].type != OBJ_SHIP && Objects[objnum].type != OBJ_START))
|
|
{
|
|
Assertion(false, "set_single_player_start() called for object %d, which is not a ship", objnum);
|
|
return false;
|
|
}
|
|
|
|
bool changed = false;
|
|
|
|
for (auto *objp : list_range(&obj_used_list))
|
|
{
|
|
if (objp->type != OBJ_SHIP && objp->type != OBJ_START)
|
|
continue;
|
|
|
|
if (OBJ_INDEX(objp) == objnum)
|
|
{
|
|
// set as player ship
|
|
if (objp->type != OBJ_START)
|
|
{
|
|
objp->type = OBJ_START;
|
|
Player_starts++;
|
|
changed = true;
|
|
}
|
|
objp->flags.set(Object::Object_Flags::Player_ship);
|
|
}
|
|
else
|
|
{
|
|
// set as regular ship
|
|
if (objp->type == OBJ_START)
|
|
{
|
|
objp->type = OBJ_SHIP;
|
|
Player_starts--;
|
|
changed = true;
|
|
}
|
|
objp->flags.remove(Object::Object_Flags::Player_ship);
|
|
}
|
|
}
|
|
|
|
ensure_valid_player_start_shipnum();
|
|
|
|
return changed;
|
|
}
|
|
|
|
SCP_string check_name_conflict(const char *entity_type, const char *name, int exclude_ship, int exclude_wing, int exclude_waypoint_list, int exclude_jump_node)
|
|
{
|
|
SCP_string msg;
|
|
|
|
// Name must not be empty
|
|
if (name[0] == '\0') {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name cannot be empty";
|
|
return msg;
|
|
}
|
|
|
|
// Check wings
|
|
for (int i = 0; i < MAX_WINGS; i++) {
|
|
if (Wings[i].wave_count && i != exclude_wing && !stricmp(Wings[i].name, name)) {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is already being used by ";
|
|
msg += (exclude_wing >= 0) ? "another wing" : "a wing";
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
// Check ships
|
|
for (auto ptr: list_range(&obj_used_list)) {
|
|
if ((ptr->type == OBJ_SHIP || ptr->type == OBJ_START) && ptr->instance != exclude_ship) {
|
|
if (!stricmp(Ships[ptr->instance].ship_name, name)) {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is already being used by ";
|
|
msg += (exclude_ship >= 0) ? "another ship" : "a ship";
|
|
return msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We don't need to check teams. "Unknown" is a valid name and also an IFF.
|
|
|
|
// Check target priority groups
|
|
for (const auto &ai_tp: Ai_tp_list) {
|
|
if (!stricmp(ai_tp.name, name)) {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is already being used by a target priority group";
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
// Check waypoint lists
|
|
int wl_size = sz2i(Waypoint_lists.size());
|
|
for (int i = 0; i < wl_size; i++) {
|
|
if (i != exclude_waypoint_list && !stricmp(Waypoint_lists[i].get_name(), name)) {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is already being used by ";
|
|
msg += (exclude_waypoint_list >= 0) ? "another waypoint path" : "a waypoint path";
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
// Check jump nodes
|
|
int jn_size = sz2i(Jump_nodes.size());
|
|
for (int i = 0; i < jn_size; i++) {
|
|
if (i != exclude_jump_node && !stricmp(Jump_nodes[i].GetName(), name)) {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is already being used by ";
|
|
msg += (exclude_jump_node >= 0) ? "another jump node" : "a jump node";
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
// Name must not start with '<' (used for invalidated SEXP references)
|
|
if (name[0] == '<') {
|
|
msg += "This ";
|
|
msg += entity_type;
|
|
msg += " name is not allowed to begin with <";
|
|
return msg;
|
|
}
|
|
|
|
return ""; // no error
|
|
}
|
|
|
|
void editor_free_engine_data()
|
|
{
|
|
// models, model instances, and collision trees; done before ship_close() because model_unload() walks Ship_info
|
|
model_free_all();
|
|
|
|
// weapon and ship table data, and ship subsystems
|
|
weapon_close();
|
|
ship_close();
|
|
|
|
// the editors hold strdup'd media names in every message, including the builtin ones
|
|
for (auto &msg : Messages)
|
|
message_free_media_names(msg);
|
|
|
|
mission_parse_close();
|
|
mission_brief_common_close();
|
|
fhash_flush();
|
|
control_config_common_close();
|
|
sexp_shutdown();
|
|
batching_shutdown();
|
|
|
|
// the cursors hold bitmaps, so this must happen before the graphics system shuts down
|
|
io::mouse::CursorManager::shutdown();
|
|
|
|
if (Cmdline_mod != nullptr)
|
|
{
|
|
delete[] Cmdline_mod;
|
|
Cmdline_mod = nullptr;
|
|
}
|
|
}
|
|
|
|
int load_and_find_campaign_mission(const char *mission_filename)
|
|
{
|
|
if (!mission_filename || !mission_filename[0])
|
|
return -1;
|
|
|
|
int idx = mission_campaign_find_mission(mission_filename);
|
|
if (idx < 0)
|
|
return -1;
|
|
|
|
if (Campaign.missions[idx].flags & CMISSION_FLAG_FRED_LOAD_PENDING)
|
|
{
|
|
read_mission_goal_list(idx);
|
|
Campaign.missions[idx].flags &= ~CMISSION_FLAG_FRED_LOAD_PENDING;
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list)
|
|
{
|
|
Assertion(Fred_running, "reassign_ship_slot is FRED-only: it does not fix up game-context references like Player_ship or runtime Ai_info shipnums");
|
|
Assertion(from != to, "reassign_ship_slot: from == to (%d)", from);
|
|
Assertion(from >= 0 && from < MAX_SHIPS, "reassign_ship_slot: 'from' slot %d out of range", from);
|
|
Assertion(to >= 0 && to < MAX_SHIPS, "reassign_ship_slot: 'to' slot %d out of range", to);
|
|
Assertion(Ships[from].objnum >= 0, "reassign_ship_slot: source slot %d is empty", from);
|
|
Assertion(Ships[to].objnum < 0, "reassign_ship_slot: destination slot %d is occupied", to);
|
|
|
|
// Move the ship struct itself. Ship's move operations handle the members
|
|
// that need care (the subsys_list sentinel re-links its bookend nodes, and
|
|
// the owning pointers are unique_ptrs); this function's job is the external
|
|
// back-references. Per the engine's convention, a slot with objnum < 0 is
|
|
// considered empty.
|
|
Ships[to] = std::move(Ships[from]);
|
|
Ships[from].objnum = -1;
|
|
|
|
// Move FRED-side parallel arrays if the caller supplied them.
|
|
if (cfg.fred_alt_names != nullptr)
|
|
{
|
|
strcpy_s(cfg.fred_alt_names[to], cfg.fred_alt_names[from]);
|
|
cfg.fred_alt_names[from][0] = '\0';
|
|
}
|
|
if (cfg.fred_callsigns != nullptr)
|
|
{
|
|
strcpy_s(cfg.fred_callsigns[to], cfg.fred_callsigns[from]);
|
|
cfg.fred_callsigns[from][0] = '\0';
|
|
}
|
|
|
|
// Object back-reference.
|
|
Objects[Ships[to].objnum].instance = to;
|
|
|
|
// Keep obj_used_list iteration order in sync with Ships[] slot order. The
|
|
// re-sort is a total rebuild rather than an incremental fix, so a caller
|
|
// making a batch of reassignments may defer it to the final call.
|
|
if (resort_obj_list)
|
|
resort_ships_in_obj_used_list();
|
|
|
|
// AI back-reference (the one invariant codified by internal_integrity_check).
|
|
if (Ships[to].ai_index >= 0)
|
|
Ai_info[Ships[to].ai_index].shipnum = to;
|
|
|
|
// Wing membership: scan every wing and re-point any reference to the old slot.
|
|
// (wing.special_ship is wing-relative, NOT a Ships[] index, so it is intentionally
|
|
// not touched here.)
|
|
for (auto &w: Wings)
|
|
{
|
|
if (w.wave_count == 0)
|
|
continue;
|
|
for (int k = 0; k < w.wave_count; ++k)
|
|
{
|
|
if (w.ship_index[k] == from)
|
|
w.ship_index[k] = to;
|
|
}
|
|
}
|
|
|
|
// Single-player start.
|
|
if (Player_start_shipnum == from)
|
|
Player_start_shipnum = to;
|
|
|
|
// Ship_registry caches the shipnum on its entries (lookup is by name, but the
|
|
// cached integer would otherwise go stale).
|
|
int reg = ship_registry_get_index(Ships[to].ship_name);
|
|
if (reg >= 0)
|
|
Ship_registry[reg].shipnum = to;
|
|
|
|
// FRED's current-ship pointer, if the caller is tracking one.
|
|
if (cfg.cur_ship != nullptr && *cfg.cur_ship == from)
|
|
*cfg.cur_ship = to;
|
|
}
|
|
|
|
static bool ship_slot_is_empty(int i)
|
|
{
|
|
return Ships[i].objnum < 0;
|
|
}
|
|
|
|
static bool wing_slot_is_empty(int i)
|
|
{
|
|
return Wings[i].wave_count == 0;
|
|
}
|
|
|
|
static int find_free_slot(int max_slots, bool (*slot_is_empty)(int))
|
|
{
|
|
for (int i = 0; i < max_slots; ++i)
|
|
{
|
|
if (slot_is_empty(i))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
template <typename TConfig>
|
|
static void swap_slots(int a, int b, const TConfig& cfg, int max_slots,
|
|
bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller)
|
|
{
|
|
if (a == b)
|
|
return;
|
|
|
|
Assertion(a >= 0 && a < max_slots, "%s: slot 'a' %d out of range", caller, a);
|
|
Assertion(b >= 0 && b < max_slots, "%s: slot 'b' %d out of range", caller, b);
|
|
Assertion(!slot_is_empty(a) && !slot_is_empty(b),
|
|
"%s: both slots must be valid (a=%d, b=%d)", caller, a, b);
|
|
|
|
// Find a free temporary slot.
|
|
int tmp = find_free_slot(max_slots, slot_is_empty);
|
|
if (tmp < 0)
|
|
{
|
|
ReleaseWarning(LOCATION, "%s: no free slot available for the temporary leg", caller);
|
|
return;
|
|
}
|
|
|
|
// Three-leg swap; each call's preconditions hold by construction. The
|
|
// total-rebuild fixups are deferred to the final leg.
|
|
reassign(a, tmp, cfg, false);
|
|
reassign(b, a, cfg, false);
|
|
reassign(tmp, b, cfg, true);
|
|
}
|
|
|
|
template <typename TConfig>
|
|
static void rotate_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const TConfig& cfg,
|
|
int max_slots, bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller)
|
|
{
|
|
if (from_pos == to_pos)
|
|
return;
|
|
|
|
int count = static_cast<int>(slots.size());
|
|
Assertion(from_pos >= 0 && from_pos < count, "%s: 'from' position %d out of range", caller, from_pos);
|
|
Assertion(to_pos >= 0 && to_pos < count, "%s: 'to' position %d out of range", caller, to_pos);
|
|
|
|
// Find a free temporary slot.
|
|
int tmp = find_free_slot(max_slots, slot_is_empty);
|
|
if (tmp < 0)
|
|
{
|
|
ReleaseWarning(LOCATION, "%s: no free slot available for the temporary leg", caller);
|
|
return;
|
|
}
|
|
|
|
// Park the moving item in the free slot, shift everything between the two
|
|
// positions over by one, then drop the item into the slot vacated at the
|
|
// far end. Preserves the relative order of everything else, in K+2
|
|
// reassignments for a move of K positions (vs 3K for a bubble of swaps).
|
|
// The total-rebuild fixups are deferred to the final leg.
|
|
int step = (to_pos > from_pos) ? 1 : -1;
|
|
reassign(slots[from_pos], tmp, cfg, false);
|
|
for (int j = from_pos; j != to_pos; j += step)
|
|
reassign(slots[j + step], slots[j], cfg, false);
|
|
reassign(tmp, slots[to_pos], cfg, true);
|
|
}
|
|
|
|
void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg)
|
|
{
|
|
swap_slots(a, b, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "swap_ship_slots");
|
|
}
|
|
|
|
void rotate_ship_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg)
|
|
{
|
|
rotate_slots(slots, from_pos, to_pos, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "rotate_ship_slots");
|
|
}
|
|
|
|
void reassign_wing_slot(int from, int to, const FredWingSlotConfig& cfg, bool update_wing_indexes)
|
|
{
|
|
Assertion(Fred_running, "reassign_wing_slot is FRED-only");
|
|
Assertion(from != to, "reassign_wing_slot: from == to (%d)", from);
|
|
Assertion(from >= 0 && from < MAX_WINGS, "reassign_wing_slot: 'from' slot %d out of range", from);
|
|
Assertion(to >= 0 && to < MAX_WINGS, "reassign_wing_slot: 'to' slot %d out of range", to);
|
|
Assertion(Wings[from].wave_count > 0, "reassign_wing_slot: source slot %d is empty", from);
|
|
Assertion(Wings[to].wave_count == 0, "reassign_wing_slot: destination slot %d is occupied", to);
|
|
|
|
// Move the wing struct itself; wave_count == 0 is the sentinel for an empty wing.
|
|
Wings[to] = std::move(Wings[from]);
|
|
Wings[from].wave_count = 0;
|
|
|
|
// Move FRED-side parallel array if the caller supplied it.
|
|
if (cfg.wing_objects != nullptr)
|
|
{
|
|
for (int k = 0; k < MAX_SHIPS_PER_WING; ++k)
|
|
{
|
|
cfg.wing_objects[to][k] = cfg.wing_objects[from][k];
|
|
cfg.wing_objects[from][k] = -1;
|
|
}
|
|
}
|
|
|
|
// Per-ship parent-wing back-reference.
|
|
for (auto &s: Ships)
|
|
{
|
|
if (s.objnum < 0)
|
|
continue;
|
|
if (s.wingnum == from)
|
|
s.wingnum = to;
|
|
}
|
|
|
|
// FRED's current-wing pointer, if the caller is tracking one.
|
|
if (cfg.cur_wing != nullptr && *cfg.cur_wing == from)
|
|
*cfg.cur_wing = to;
|
|
|
|
// Rebuild Starting/Squadron/TVT_wings caches from the parallel name arrays.
|
|
// The rebuild is total rather than incremental, so a caller making a batch
|
|
// of reassignments may defer it to the final call.
|
|
if (update_wing_indexes)
|
|
update_custom_wing_indexes();
|
|
}
|
|
|
|
void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg)
|
|
{
|
|
swap_slots(a, b, cfg, MAX_WINGS, wing_slot_is_empty, reassign_wing_slot, "swap_wing_slots");
|
|
}
|
|
|
|
void rotate_wing_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg)
|
|
{
|
|
rotate_slots(slots, from_pos, to_pos, cfg, MAX_WINGS, wing_slot_is_empty, reassign_wing_slot, "rotate_wing_slots");
|
|
}
|
|
|
|
// Bulk-re-sort one type's subset of obj_used_list while keeping non-matching
|
|
// entries in their original relative positions. Each callsite supplies a
|
|
// type matcher and a key function; the i-th matching slot (in original list
|
|
// order) receives the i-th smallest matching node by key.
|
|
static void resort_obj_used_list_subset(
|
|
bool (*matches_type)(int),
|
|
int (*key)(const object*))
|
|
{
|
|
SCP_vector<object*> all;
|
|
SCP_vector<object*> matched;
|
|
for (auto o : list_range(&obj_used_list))
|
|
{
|
|
all.push_back(o);
|
|
if (matches_type(o->type))
|
|
matched.push_back(o);
|
|
}
|
|
|
|
std::sort(matched.begin(), matched.end(),
|
|
[&](const object* a, const object* b) { return key(a) < key(b); });
|
|
|
|
list_init(&obj_used_list);
|
|
auto it = matched.begin();
|
|
for (auto o : all)
|
|
{
|
|
if (matches_type(o->type))
|
|
{
|
|
list_append(&obj_used_list, *it);
|
|
++it;
|
|
}
|
|
else
|
|
{
|
|
list_append(&obj_used_list, o);
|
|
}
|
|
}
|
|
}
|
|
|
|
void resort_ships_in_obj_used_list()
|
|
{
|
|
resort_obj_used_list_subset(
|
|
[](int t) { return t == OBJ_SHIP || t == OBJ_START; },
|
|
[](const object* o) { return o->instance; });
|
|
}
|
|
|
|
void resort_props_in_obj_used_list()
|
|
{
|
|
resort_obj_used_list_subset(
|
|
[](int t) { return t == OBJ_PROP; },
|
|
[](const object* o) { return o->instance; });
|
|
}
|
|
|
|
void rotate_prop_slots(const SCP_vector<int>& slots, int from_pos, int to_pos)
|
|
{
|
|
Assertion(Fred_running, "rotate_prop_slots is FRED-only: it re-points object instances only, not any game-context state");
|
|
if (from_pos == to_pos)
|
|
return;
|
|
|
|
int count = static_cast<int>(slots.size());
|
|
Assertion(from_pos >= 0 && from_pos < count, "rotate_prop_slots: 'from' position %d out of range", from_pos);
|
|
Assertion(to_pos >= 0 && to_pos < count, "rotate_prop_slots: 'to' position %d out of range", to_pos);
|
|
|
|
// Permute the prop occupants among the slot indices, leaving any empty
|
|
// holes where they are. Park the mover, shift the rest over by one, then drop
|
|
// the mover into the slot vacated at the far end.
|
|
std::optional<prop> moving = std::move(Props[slots[from_pos]]);
|
|
int step = (to_pos > from_pos) ? 1 : -1;
|
|
for (int j = from_pos; j != to_pos; j += step)
|
|
Props[slots[j]] = std::move(Props[slots[j + step]]);
|
|
Props[slots[to_pos]] = std::move(moving);
|
|
|
|
// Re-point each moved prop's object instance to its new Props[] index. Only
|
|
// the occupants between the two positions changed slots.
|
|
int lo = std::min(from_pos, to_pos);
|
|
int hi = std::max(from_pos, to_pos);
|
|
for (int j = lo; j <= hi; ++j)
|
|
{
|
|
if (Props[slots[j]].has_value())
|
|
Objects[Props[slots[j]]->objnum].instance = slots[j];
|
|
}
|
|
|
|
// Keep obj_used_list prop order in sync with Props[] index order, so
|
|
// the Scene Browser reflects the new order too.
|
|
resort_props_in_obj_used_list();
|
|
}
|
|
|
|
void rotate_waypoint_lists(int from_pos, int to_pos)
|
|
{
|
|
Assertion(Fred_running, "rotate_waypoint_lists is FRED-only: it re-points object instances only, not any game-context state");
|
|
if (from_pos == to_pos)
|
|
return;
|
|
|
|
int count = static_cast<int>(Waypoint_lists.size());
|
|
Assertion(from_pos >= 0 && from_pos < count, "rotate_waypoint_lists: 'from' position %d out of range", from_pos);
|
|
Assertion(to_pos >= 0 && to_pos < count, "rotate_waypoint_lists: 'to' position %d out of range", to_pos);
|
|
|
|
// Reorder the lists themselves, preserving the relative order of the rest.
|
|
if (from_pos < to_pos)
|
|
std::rotate(Waypoint_lists.begin() + from_pos, Waypoint_lists.begin() + from_pos + 1, Waypoint_lists.begin() + to_pos + 1);
|
|
else
|
|
std::rotate(Waypoint_lists.begin() + to_pos, Waypoint_lists.begin() + from_pos, Waypoint_lists.begin() + from_pos + 1);
|
|
|
|
// A waypoint object encodes its list index, point index in its instance, so
|
|
// every waypoint in the shifted lists now sits at a new list index. Rebuild
|
|
// all instances from the lists' current positions.
|
|
int li = 0;
|
|
for (auto& list : Waypoint_lists)
|
|
{
|
|
int wi = 0;
|
|
for (auto& wpt : list.get_waypoints())
|
|
{
|
|
int objnum = wpt.get_objnum();
|
|
if (objnum >= 0)
|
|
Objects[objnum].instance = calc_waypoint_instance(li, wi);
|
|
++wi;
|
|
}
|
|
++li;
|
|
}
|
|
}
|
|
|
|
void rotate_jump_nodes(const SCP_vector<int>& slots, int from_pos, int to_pos)
|
|
{
|
|
Assertion(Fred_running, "rotate_jump_nodes is FRED-only: it reorders the editor's jump-node list only");
|
|
if (from_pos == to_pos)
|
|
return;
|
|
|
|
int count = static_cast<int>(slots.size());
|
|
Assertion(from_pos >= 0 && from_pos < count, "rotate_jump_nodes: 'from' position %d out of range", from_pos);
|
|
Assertion(to_pos >= 0 && to_pos < count, "rotate_jump_nodes: 'to' position %d out of range", to_pos);
|
|
|
|
// Permute the node occupants among the fixed slot indices, leaving any
|
|
// detached entries in place. Park the mover, shift the rest over by one, then
|
|
// drop the mover into the slot vacated at the far end.
|
|
CJumpNode moving = std::move(Jump_nodes[slots[from_pos]]);
|
|
int step = (to_pos > from_pos) ? 1 : -1;
|
|
for (int j = from_pos; j != to_pos; j += step)
|
|
Jump_nodes[slots[j]] = std::move(Jump_nodes[slots[j + step]]);
|
|
Jump_nodes[slots[to_pos]] = std::move(moving);
|
|
}
|