Files
fs2open.github.com/code/missioneditor/common.cpp
T
Goober5000andClaude Opus 5 a159e67a10 share the loadout usage-list code between the two editors
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]>
2026-09-25 03:22:23 -04:00

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);
}