diff --git a/code/missioneditor/common.cpp b/code/missioneditor/common.cpp index ce3293e10a5..a78c3c058d8 100644 --- a/code/missioneditor/common.cpp +++ b/code/missioneditor/common.cpp @@ -1,17 +1,16 @@ // methods and members common to any mission editor FSO may have #include "common.h" #include "ai/ai.h" +#include "ai/aigoals.h" #include "globalincs/linklist.h" #include "mission/missionparse.h" #include "iff_defs/iff_defs.h" #include "jumpnode/jumpnode.h" #include "object/object.h" #include "object/waypoint.h" -#include "prop/prop.h" +#include "parse/sexp.h" #include "ship/ship.h" -#include - // to keep track of data char Voice_abbrev_briefing[NAME_LENGTH]; char Voice_abbrev_campaign[NAME_LENGTH]; @@ -318,352 +317,14 @@ SCP_string check_name_conflict(const char *entity_type, const char *name, int ex return ""; // no error } -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) +// Update AI goal references in every Ai_info entry and every wing. +void ai_update_goal_references(sexp_ref_type type, const char *old_name, const char *new_name) { - 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 -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 -static void rotate_slots(const SCP_vector& 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(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& 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& 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 all; - SCP_vector 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& 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(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 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(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& 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; + for (int i = 0; i < MAX_AI_INFO; i++) // loop through all Ai_info entries + if (Ai_info[i].shipnum >= 0) // skip if unused + ai_update_goal_references(Ai_info[i].goals, type, old_name, new_name); - int count = static_cast(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); + for (auto &w : Wings) // loop through all wings + if (w.wave_count > 0) // skip if unpopulated + ai_update_goal_references(w.ai_goals, type, old_name, new_name); } diff --git a/code/missioneditor/common.h b/code/missioneditor/common.h index 6622afcece0..940687add23 100644 --- a/code/missioneditor/common.h +++ b/code/missioneditor/common.h @@ -71,86 +71,7 @@ bool set_single_player_start(int objnum); // against the entity currently being renamed. SCP_string check_name_conflict(const char *entity_type, const char *name, int exclude_ship = -1, int exclude_wing = -1, int exclude_waypoint_list = -1, int exclude_jump_node = -1); -struct FredShipSlotConfig -{ - char (*fred_alt_names)[NAME_LENGTH + 1] = nullptr; - char (*fred_callsigns)[NAME_LENGTH + 1] = nullptr; +enum class sexp_ref_type; - int *cur_ship = nullptr; -}; - -// Move the ship currently in Ships[from] into Ships[to], updating every -// back-reference (Objects, Ai_info, Wings, Player_start_shipnum, Ship_registry, -// and editor-side fields supplied via cfg). Leaves Ships[from] empty. -// Preconditions: from != to, Ships[from].objnum >= 0, Ships[to].objnum < 0. -// No caller may hold a ship* to either slot across this call. -// Fields in cfg whose pointers are nullptr are skipped. -void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list = true); - -// Swap the contents of two slots. Both must be valid (Ships[a].objnum >= 0 -// and Ships[b].objnum >= 0). Implemented as three calls to reassign_ship_slot -// via a temporary empty slot. -void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg); - -// Move the item at position from_pos in slots to position to_pos, shifting the -// items in between by one position. -void rotate_ship_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg); - -struct FredWingSlotConfig -{ - int (*wing_objects)[MAX_SHIPS_PER_WING] = nullptr; - int *cur_wing = nullptr; -}; - -// Move the wing currently in Wings[from] into Wings[to], updating every -// back-reference (Ships[i].wingnum, Starting/Squadron/TVT_wings caches, and -// editor-side fields supplied via cfg). Leaves Wings[from] empty. -// Preconditions: from != to, Wings[from].wave_count > 0, Wings[to].wave_count == 0. -// No caller may hold a wing* to either slot across this call. -// Fields in cfg whose pointers are nullptr are skipped. -void reassign_wing_slot(int from, int to, const FredWingSlotConfig& cfg, bool update_wing_indexes = true); - -// Swap the contents of two slots. Both must be valid (Wings[a].wave_count > 0 -// and Wings[b].wave_count > 0). Implemented as three calls to -// reassign_wing_slot via a temporary empty slot. -void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg); - -// Move the item at position from_pos in slots to position to_pos, shifting the -// items in between by one position. -void rotate_wing_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg); - -// Restore the obj_used_list invariant for the OBJ_SHIP/OBJ_START subset: -// among ship-type entries, list order matches Ships[] index order. -void resort_ships_in_obj_used_list(); - -// Same, for the OBJ_PROP subset: among prop entries, obj_used_list order matches -// Props[] index order. UI lists that walk obj_used_list, like the Scene Browser, -// depend on this to show props in their reordered Props[] order. -void resort_props_in_obj_used_list(); - -// Props and waypoint paths live in SCP_vectors rather than the fixed -// Ships[]/Wings[] arrays, and their only index-based back-reference is the -// owning object's instance (everything else refers to them by name). So unlike -// the ship/wing slot helpers above, these reorder the elements directly and just -// re-point the OBJ_PROP / OBJ_WAYPOINT object instances. - -// Move the prop at display position from_pos to to_pos within `slots` (the -// occupied Props[] indices, in display order), shifting the props in between by -// one and preserving their relative order. Props[] may contain empty nullopt -// holes; those stay put while the occupants are permuted among their slots. -// Also resorts obj_used_list so Scene Browser matches. -void rotate_prop_slots(const SCP_vector& slots, int from_pos, int to_pos); - -// Move the waypoint list at index from_pos to to_pos within Waypoint_lists, -// shifting the lists in between by one and preserving their relative order. -// Waypoint_lists is kept compact, so positions are plain indices. -void rotate_waypoint_lists(int from_pos, int to_pos); - -// Move the jump node at display position from_pos to to_pos within `slots` (the -// live Jump_nodes[] indices, in display order), shifting the nodes in between by -// one and preserving their relative order. Unlike ships/props there is no -// object-instance back-reference to re-point: a jump node links to its object via -// CJumpNode::m_objnum, which travels with the node, and is looked up by -// objnum/name, and the Scene Browser and mission save both iterate Jump_nodes -// directly so permuting the occupants among their slots is all that is needed. -void rotate_jump_nodes(const SCP_vector& slots, int from_pos, int to_pos); +// Update AI goal references in every Ai_info entry and every wing. +void ai_update_goal_references(sexp_ref_type type, const char *old_name, const char *new_name); diff --git a/code/missioneditor/slot_operations.cpp b/code/missioneditor/slot_operations.cpp new file mode 100644 index 00000000000..0249ad9ad00 --- /dev/null +++ b/code/missioneditor/slot_operations.cpp @@ -0,0 +1,697 @@ +#include "slot_operations.h" +#include "common.h" +#include "ai/ai.h" +#include "globalincs/linklist.h" +#include "mission/missionparse.h" +#include "jumpnode/jumpnode.h" +#include "object/object.h" +#include "object/waypoint.h" +#include "parse/sexp.h" +#include "prop/prop.h" +#include "ship/ship.h" + +#include + +// Forward declarations ------- + +// Restore the obj_used_list invariant for the OBJ_SHIP/OBJ_START subset: +// among ship-type entries, list order matches Ships[] index order. +static void resort_ships_in_obj_used_list(); + +// Same, for the OBJ_PROP subset: among prop entries, obj_used_list order matches +// Props[] index order. UI lists that walk obj_used_list, like the Scene Browser, +// depend on this to show props in their reordered Props[] order. +static void resort_props_in_obj_used_list(); + +// Restore the OBJ_WAYPOINT subset invariant: list order matches the composite +// (waypoint_list_index, waypoint_index) order encoded in Objects[].instance +// via calc_waypoint_instance. +static void resort_waypoints_in_obj_used_list(); + +// Restore the OBJ_JUMP_NODE subset invariant: list order matches Jump_nodes[] +// vector order. Lookup is by CJumpNode::GetSCPObjectNumber() since +// Objects[].instance is unused for jump nodes. +static void resort_jump_nodes_in_obj_used_list(); + +// ---------------------------- + +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; +} + +// How the swap/rotate templates obtain (and, for vector-backed storage, give +// back) the temporary slot used to park the moving item, plus the bounds and +// emptiness test for the storage. Fixed arrays scan for an existing hole and +// have nothing to release; vector-backed types append a fresh empty element, +// so acquisition never fails. +struct SlotOps +{ + int max_slots; // one-past-max valid slot index (snapshot taken before temp acquisition) + bool (*slot_is_empty)(int); + int (*acquire_temp_slot)(); // returns an empty slot to park the mover in, or -1 if none + void (*release_temp_slot)(int); // nullptr if there is nothing to release (fixed arrays) +}; + +template +static void swap_slots(int a, int b, const TConfig& cfg, const SlotOps& ops, + void (*reassign)(int, int, const TConfig&, bool), const char* caller) +{ + if (a == b) + return; + + Assertion(a >= 0 && a < ops.max_slots, "%s: slot 'a' %d out of range", caller, a); + Assertion(b >= 0 && b < ops.max_slots, "%s: slot 'b' %d out of range", caller, b); + Assertion(!ops.slot_is_empty(a) && !ops.slot_is_empty(b), + "%s: both slots must be valid (a=%d, b=%d)", caller, a, b); + + // Obtain a free temporary slot. + int tmp = ops.acquire_temp_slot(); + 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); + + if (ops.release_temp_slot != nullptr) + ops.release_temp_slot(tmp); +} + +template +static void rotate_slots(const SCP_vector& slots, int from_pos, int to_pos, const TConfig& cfg, + const SlotOps& ops, void (*reassign)(int, int, const TConfig&, bool), const char* caller) +{ + if (from_pos == to_pos) + return; + + int count = sz2i(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); + + // Obtain a free temporary slot. + int tmp = ops.acquire_temp_slot(); + 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); + + if (ops.release_temp_slot != nullptr) + ops.release_temp_slot(tmp); +} + +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 int ship_acquire_temp_slot() +{ + return find_free_slot(MAX_SHIPS, ship_slot_is_empty); +} + +static SlotOps ship_slot_ops() +{ + return { MAX_SHIPS, ship_slot_is_empty, ship_acquire_temp_slot, nullptr }; +} + +void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg) +{ + swap_slots(a, b, cfg, ship_slot_ops(), reassign_ship_slot, "swap_ship_slots"); +} + +void rotate_ship_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg) +{ + rotate_slots(slots, from_pos, to_pos, cfg, ship_slot_ops(), 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(); +} + +static bool wing_slot_is_empty(int i) +{ + return Wings[i].wave_count == 0; +} + +static int wing_acquire_temp_slot() +{ + return find_free_slot(MAX_WINGS, wing_slot_is_empty); +} + +static SlotOps wing_slot_ops() +{ + return { MAX_WINGS, wing_slot_is_empty, wing_acquire_temp_slot, nullptr }; +} + +void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg) +{ + swap_slots(a, b, cfg, wing_slot_ops(), reassign_wing_slot, "swap_wing_slots"); +} + +void rotate_wing_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg) +{ + rotate_slots(slots, from_pos, to_pos, cfg, wing_slot_ops(), reassign_wing_slot, "rotate_wing_slots"); +} + +static bool prop_slot_is_empty(int i) +{ + return !Props[i].has_value(); +} + +static int prop_acquire_temp_slot() +{ + // Reuse an existing hole if one is available; otherwise append one. + int i = find_free_slot(sz2i(Props.size()), prop_slot_is_empty); + if (i >= 0) + return i; + Props.emplace_back(std::nullopt); + return sz2i(Props.size()) - 1; +} + +static void prop_release_temp_slot(int i) +{ + // Only shrink if the temp slot was appended; a reused hole stays put. + if (i == sz2i(Props.size()) - 1 && !Props[i].has_value()) + Props.pop_back(); +} + +static SlotOps prop_slot_ops() +{ + return { sz2i(Props.size()), prop_slot_is_empty, prop_acquire_temp_slot, prop_release_temp_slot }; +} + +static void reassign_prop_slot(int from, int to, const FredPropSlotConfig& /*cfg*/, bool resort_obj_list) +{ + Assertion(Fred_running, "reassign_prop_slot is FRED-only: it re-points object instances only, not any game-context state"); + Assertion(from != to, "reassign_prop_slot: from == to (%d)", from); + Assertion(SCP_vector_inbounds(Props, from), "reassign_prop_slot: 'from' slot %d out of range", from); + Assertion(SCP_vector_inbounds(Props, to), "reassign_prop_slot: 'to' slot %d out of range", to); + Assertion(Props[from].has_value(), "reassign_prop_slot: source slot %d is empty", from); + Assertion(!Props[to].has_value(), "reassign_prop_slot: destination slot %d is occupied", to); + + // Move the prop itself. A moved-from optional is still engaged, so the + // explicit reset() is what actually empties the source slot. + Props[to] = std::move(Props[from]); + Props[from].reset(); + + // Object back-reference. + Objects[Props[to]->objnum].instance = to; + + // Keep obj_used_list prop order in sync with Props[] index order, so UI + // lists that walk it (like the Scene Browser) reflect the new order. The + // re-sort is a total rebuild, so a caller making a batch of reassignments + // may defer it to the final call. + if (resort_obj_list) + resort_props_in_obj_used_list(); +} + +void swap_prop_slots(int a, int b, const FredPropSlotConfig& cfg) +{ + swap_slots(a, b, cfg, prop_slot_ops(), reassign_prop_slot, "swap_prop_slots"); +} + +void rotate_prop_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredPropSlotConfig& cfg) +{ + rotate_slots(slots, from_pos, to_pos, cfg, prop_slot_ops(), reassign_prop_slot, "rotate_prop_slots"); +} + +void rederive_cur_waypoint(const FredWaypointConfig& cfg) +{ + if (cfg.cur_waypoint == nullptr || cfg.cur_waypoint_list == nullptr || cfg.cur_object_index == nullptr) + return; + if (*cfg.cur_waypoint == nullptr) + return; + + int idx = *cfg.cur_object_index; + if (idx < 0 || idx >= MAX_OBJECTS || Objects[idx].type != OBJ_WAYPOINT) + { + // the stable key says no waypoint is selected, so any cached pointer is stale + *cfg.cur_waypoint = nullptr; + *cfg.cur_waypoint_list = nullptr; + return; + } + + *cfg.cur_waypoint = find_waypoint_with_instance(Objects[idx].instance); + *cfg.cur_waypoint_list = (*cfg.cur_waypoint != nullptr) ? (*cfg.cur_waypoint)->get_parent_list() : nullptr; +} + +static bool waypoint_list_slot_is_empty(int i) +{ + // The engine keeps Waypoint_lists compact (removing a list's last waypoint + // removes the list), so an empty list only ever exists as the templates' + // temporary slot. + return Waypoint_lists[i].get_waypoints().empty(); +} + +static int waypoint_list_acquire_temp_slot() +{ + Waypoint_lists.emplace_back(); + return sz2i(Waypoint_lists.size()) - 1; +} + +static void waypoint_list_release_temp_slot(int i) +{ + Assertion(i == sz2i(Waypoint_lists.size()) - 1 && waypoint_list_slot_is_empty(i), + "waypoint_list_release_temp_slot: slot %d is not the appended temporary", i); + Waypoint_lists.pop_back(); +} + +static SlotOps waypoint_list_slot_ops() +{ + return { sz2i(Waypoint_lists.size()), waypoint_list_slot_is_empty, waypoint_list_acquire_temp_slot, waypoint_list_release_temp_slot }; +} + +static void reassign_waypoint_list_slot(int from, int to, const FredWaypointConfig& cfg, bool final_leg) +{ + Assertion(Fred_running, "reassign_waypoint_list_slot is FRED-only: it re-points object instances only, not any game-context state"); + Assertion(from != to, "reassign_waypoint_list_slot: from == to (%d)", from); + Assertion(SCP_vector_inbounds(Waypoint_lists, from), "reassign_waypoint_list_slot: 'from' slot %d out of range", from); + Assertion(SCP_vector_inbounds(Waypoint_lists, to), "reassign_waypoint_list_slot: 'to' slot %d out of range", to); + Assertion(!waypoint_list_slot_is_empty(from), "reassign_waypoint_list_slot: source slot %d is empty", from); + Assertion(waypoint_list_slot_is_empty(to), "reassign_waypoint_list_slot: destination slot %d is occupied", to); + + // Move the list itself. waypoint_list's move operations are defaulted and + // its name is a char array, which copies — so the moved-from slot still + // carries the old name and the explicit re-init is what empties it. + Waypoint_lists[to] = std::move(Waypoint_lists[from]); + Waypoint_lists[from] = waypoint_list(); + + // A waypoint object encodes its (list index, point index) in its instance, + // so every waypoint in the moved list now sits at a new list index. + int wi = 0; + for (auto& wpt : Waypoint_lists[to].get_waypoints()) + { + int objnum = wpt.get_objnum(); + if (objnum >= 0) + Objects[objnum].instance = calc_waypoint_instance(to, wi); + ++wi; + } + + // Total fixups, deferred to the final leg of a batch: keep obj_used_list + // waypoint order in sync with the instance encoding, and re-derive the + // editor's cur_waypoint/cur_waypoint_list pointers (which now point into + // reshuffled, possibly reallocated storage) from the stable object index. + if (final_leg) + { + resort_waypoints_in_obj_used_list(); + rederive_cur_waypoint(cfg); + } +} + +void swap_waypoint_lists(int a, int b, const FredWaypointConfig& cfg) +{ + swap_slots(a, b, cfg, waypoint_list_slot_ops(), reassign_waypoint_list_slot, "swap_waypoint_lists"); +} + +void rotate_waypoint_lists(int from_pos, int to_pos, const FredWaypointConfig& cfg) +{ + // Waypoint_lists is compact, so display positions are the slot indices. + SCP_vector slots(Waypoint_lists.size()); + for (int i = 0; i < sz2i(slots.size()); ++i) + slots[i] = i; + + rotate_slots(slots, from_pos, to_pos, cfg, waypoint_list_slot_ops(), reassign_waypoint_list_slot, "rotate_waypoint_lists"); +} + +static bool jump_node_slot_is_empty(int i) +{ + // The engine keeps Jump_nodes compact (deletion erases the element), so a + // detached node only ever exists as the templates' temporary slot or + // transiently during deletion. + return Jump_nodes[i].GetSCPObjectNumber() < 0; +} + +static int jump_node_acquire_temp_slot() +{ + // A default-constructed CJumpNode is cheap: no model load, no object. + Jump_nodes.emplace_back(); + return sz2i(Jump_nodes.size()) - 1; +} + +static void jump_node_release_temp_slot(int i) +{ + Assertion(i == sz2i(Jump_nodes.size()) - 1 && jump_node_slot_is_empty(i), + "jump_node_release_temp_slot: slot %d is not the appended temporary", i); + Jump_nodes.pop_back(); +} + +static SlotOps jump_node_slot_ops() +{ + return { sz2i(Jump_nodes.size()), jump_node_slot_is_empty, jump_node_acquire_temp_slot, jump_node_release_temp_slot }; +} + +static void reassign_jump_node_slot(int from, int to, const FredJumpNodeSlotConfig& /*cfg*/, bool resort_obj_list) +{ + Assertion(Fred_running, "reassign_jump_node_slot is FRED-only: it reorders the editor's jump-node list only"); + Assertion(from != to, "reassign_jump_node_slot: from == to (%d)", from); + Assertion(SCP_vector_inbounds(Jump_nodes, from), "reassign_jump_node_slot: 'from' slot %d out of range", from); + Assertion(SCP_vector_inbounds(Jump_nodes, to), "reassign_jump_node_slot: 'to' slot %d out of range", to); + Assertion(!jump_node_slot_is_empty(from), "reassign_jump_node_slot: source slot %d is empty", from); + Assertion(jump_node_slot_is_empty(to), "reassign_jump_node_slot: destination slot %d is occupied", to); + + // Move the node itself. CJumpNode's move operations are defaulted, so the + // scalar m_objnum is copied and the moved-from node would still look + // attached — the explicit re-init is what empties the source slot. (Safe: + // the destructor frees nothing; obj/model cleanup happens in + // jumpnode_delete(), per the class comment.) + Jump_nodes[to] = std::move(Jump_nodes[from]); + Jump_nodes[from] = CJumpNode(); + + // No object back-reference to re-point: the node links to its object via + // m_objnum, which travels with it, and Objects[].instance is unused for + // jump nodes. + + // Keep obj_used_list jump-node order in sync with Jump_nodes[] order, so UI + // lists that walk it reflect the new order. Deferred to the final leg of a + // batch. + if (resort_obj_list) + resort_jump_nodes_in_obj_used_list(); +} + +void swap_jump_node_slots(int a, int b, const FredJumpNodeSlotConfig& cfg) +{ + swap_slots(a, b, cfg, jump_node_slot_ops(), reassign_jump_node_slot, "swap_jump_node_slots"); +} + +void rotate_jump_node_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredJumpNodeSlotConfig& cfg) +{ + rotate_slots(slots, from_pos, to_pos, cfg, jump_node_slot_ops(), reassign_jump_node_slot, "rotate_jump_node_slots"); +} + +// Individual waypoints are referenced in SEXPs and AI goals positionally by +// name ("listname:N", 1-based), so reordering waypoints within a list must +// rename every affected reference. Renaming in place would collide when a +// name is both a source and a destination, so the helpers below route through +// temporary names (""): rename affected refs to temps, shift the +// positions, then rename the temps to their final names. + +static void rename_waypoint_sexp_refs_to_temp(const char *list_name, int one_based, char *temp_buf, size_t buf_size) +{ + char name[NAME_LENGTH]; + waypoint_stuff_name(name, list_name, one_based); + snprintf(temp_buf, buf_size, "", one_based); + update_sexp_references(name, temp_buf); + ai_update_goal_references(sexp_ref_type::WAYPOINT, name, temp_buf); +} + +static void rename_waypoint_sexp_refs_from_temp(const char *temp_name, const char *list_name, int new_1based) +{ + char new_name[NAME_LENGTH]; + waypoint_stuff_name(new_name, list_name, new_1based); + update_sexp_references(temp_name, new_name); + ai_update_goal_references(sexp_ref_type::WAYPOINT, temp_name, new_name); +} + +void move_waypoint_within_list(int list_index, int from, int to) +{ + Assertion(Fred_running, "move_waypoint_within_list is FRED-only: it rewrites editor-side SEXP and AI goal references"); + Assertion(SCP_vector_inbounds(Waypoint_lists, list_index), "move_waypoint_within_list: list index %d out of range", list_index); + + auto &wl = Waypoint_lists[list_index]; + auto list_name = wl.get_name(); + auto &wpts = wl.get_waypoints(); + + Assertion(wpts.in_bounds(from), "move_waypoint_within_list: 'from' index %d out of range", from); + Assertion(wpts.in_bounds(to), "move_waypoint_within_list: 'to' index %d out of range", to); + if (from == to) + return; + + int lo = std::min(from, to); + int hi = std::max(from, to); + + // Step 1: Rename all affected SEXP refs to temporary names + SCP_vector temp_names(hi - lo + 1); + for (int i = lo; i <= hi; i++) + { + char temp[NAME_LENGTH + 16]; + rename_waypoint_sexp_refs_to_temp(list_name, i + 1, temp, sizeof(temp)); + temp_names[i - lo] = temp; + } + + // Step 2: Shift positions (waypoint objects stay in place) + vec3d saved_pos = *wpts[from].get_pos(); + if (from < to) + { + for (int i = from; i < to; i++) + wpts[i].set_pos(wpts[i + 1].get_pos()); + } + else + { + for (int i = from; i > to; i--) + wpts[i].set_pos(wpts[i - 1].get_pos()); + } + wpts[to].set_pos(&saved_pos); + + // Step 3: Rename from temp names to final (shifted) names. + // temp_names[i - lo] holds the temp name for what was originally at 0-based index i. + // Map each original index to its new index after the move: + // - The element at 'from' moved to 'to' + // - If from < to: elements at from+1..to shifted down by 1 + // - If from > to: elements at to..from-1 shifted up by 1 + for (int i = lo; i <= hi; i++) + { + int new_index; + if (i == from) + new_index = to; + else if (from < to) + new_index = i - 1; // shifted down + else + new_index = i + 1; // shifted up + rename_waypoint_sexp_refs_from_temp(temp_names[i - lo].c_str(), list_name, new_index + 1); + } +} + +void swap_waypoints_within_list(int list_index, int a, int b) +{ + Assertion(Fred_running, "swap_waypoints_within_list is FRED-only: it rewrites editor-side SEXP and AI goal references"); + Assertion(SCP_vector_inbounds(Waypoint_lists, list_index), "swap_waypoints_within_list: list index %d out of range", list_index); + + auto &wl = Waypoint_lists[list_index]; + auto list_name = wl.get_name(); + auto &wpts = wl.get_waypoints(); + + Assertion(wpts.in_bounds(a), "swap_waypoints_within_list: index 'a' %d out of range", a); + Assertion(wpts.in_bounds(b), "swap_waypoints_within_list: index 'b' %d out of range", b); + if (a == b) + return; + + // Step 1: Rename both SEXP refs to temp names + char temp_a[NAME_LENGTH + 16]; + char temp_b[NAME_LENGTH + 16]; + rename_waypoint_sexp_refs_to_temp(list_name, a + 1, temp_a, sizeof(temp_a)); + rename_waypoint_sexp_refs_to_temp(list_name, b + 1, temp_b, sizeof(temp_b)); + + // Step 2: Swap positions (waypoint objects stay in place) + vec3d pos_a = *wpts[a].get_pos(); + vec3d pos_b = *wpts[b].get_pos(); + wpts[a].set_pos(&pos_b); + wpts[b].set_pos(&pos_a); + + // Step 3: Rename from temp to final (swapped positions) + rename_waypoint_sexp_refs_from_temp(temp_a, list_name, b + 1); + rename_waypoint_sexp_refs_from_temp(temp_b, list_name, a + 1); +} + +// 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 all; + SCP_vector 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); + } + } +} + +static 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; }); +} + +static void resort_props_in_obj_used_list() +{ + resort_obj_used_list_subset( + [](int t) { return t == OBJ_PROP; }, + [](const object* o) { return o->instance; }); +} + +static void resort_waypoints_in_obj_used_list() +{ + resort_obj_used_list_subset( + [](int t) { return t == OBJ_WAYPOINT; }, + [](const object* o) { return o->instance; }); +} + +static void resort_jump_nodes_in_obj_used_list() +{ + resort_obj_used_list_subset( + [](int t) { return t == OBJ_JUMP_NODE; }, + [](const object* o) + { + for (int i = 0; i < (int)Jump_nodes.size(); ++i) + if (Jump_nodes[i].GetSCPObjectNumber() == OBJ_INDEX(o)) + return i; + + return INT_MAX; + }); +} diff --git a/code/missioneditor/slot_operations.h b/code/missioneditor/slot_operations.h new file mode 100644 index 00000000000..087426a97a1 --- /dev/null +++ b/code/missioneditor/slot_operations.h @@ -0,0 +1,130 @@ +#pragma once +#include "globalincs/globals.h" + +struct FredShipSlotConfig +{ + char (*fred_alt_names)[NAME_LENGTH + 1] = nullptr; + char (*fred_callsigns)[NAME_LENGTH + 1] = nullptr; + + int *cur_ship = nullptr; +}; + +// Move the ship currently in Ships[from] into Ships[to], updating every +// back-reference (Objects, Ai_info, Wings, Player_start_shipnum, Ship_registry, +// and editor-side fields supplied via cfg). Leaves Ships[from] empty. +// Preconditions: from != to, Ships[from].objnum >= 0, Ships[to].objnum < 0. +// No caller may hold a ship* to either slot across this call. +// Fields in cfg whose pointers are nullptr are skipped. +void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list = true); + +// Swap the contents of two slots. Both must be valid (Ships[a].objnum >= 0 +// and Ships[b].objnum >= 0). Implemented as three calls to reassign_ship_slot +// via a temporary empty slot. +void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg); + +// Move the item at position from_pos in slots to position to_pos, shifting the +// items in between by one position. +void rotate_ship_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg); + +struct FredWingSlotConfig +{ + int (*wing_objects)[MAX_SHIPS_PER_WING] = nullptr; + int *cur_wing = nullptr; +}; + +// Move the wing currently in Wings[from] into Wings[to], updating every +// back-reference (Ships[i].wingnum, Starting/Squadron/TVT_wings caches, and +// editor-side fields supplied via cfg). Leaves Wings[from] empty. +// Preconditions: from != to, Wings[from].wave_count > 0, Wings[to].wave_count == 0. +// No caller may hold a wing* to either slot across this call. +// Fields in cfg whose pointers are nullptr are skipped. +void reassign_wing_slot(int from, int to, const FredWingSlotConfig& cfg, bool update_wing_indexes = true); + +// Swap the contents of two slots. Both must be valid (Wings[a].wave_count > 0 +// and Wings[b].wave_count > 0). Implemented as three calls to +// reassign_wing_slot via a temporary empty slot. +void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg); + +// Move the item at position from_pos in slots to position to_pos, shifting the +// items in between by one position. +void rotate_wing_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg); + +struct FredPropSlotConfig +{ + // No editor-side prop state needs fixups today (the only index-based + // back-reference is Objects[].instance, handled internally). Reserved for + // future fields. +}; + +// Swap the props in Props[] slots a and b. Both slots must be occupied +// (Props[i].has_value()). Also resorts obj_used_list so UI lists that walk it, +// like the Scene Browser, match. +void swap_prop_slots(int a, int b, const FredPropSlotConfig& cfg); + +// Move the prop at display position from_pos to to_pos within `slots` (the +// occupied Props[] indices, in display order), shifting the props in between by +// one and preserving their relative order. Props[] may contain empty nullopt +// holes; those stay put while the occupants are permuted among their slots. +// Also resorts obj_used_list so Scene Browser matches. +void rotate_prop_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredPropSlotConfig& cfg); + +class waypoint; +class waypoint_list; + +struct FredWaypointConfig +{ + // The editor's current-waypoint tracking is raw pointers into vectors that + // this machinery reorders (and possibly reallocates), so they are re-derived + // after the operation from the stable object index. Fields whose pointers + // are nullptr are skipped. + waypoint** cur_waypoint = nullptr; + waypoint_list** cur_waypoint_list = nullptr; + const int* cur_object_index = nullptr; // FRED2: &cur_object_index; qtFRED: &editor.currentObject +}; + +// Re-derive *cfg.cur_waypoint and *cfg.cur_waypoint_list from +// *cfg.cur_object_index via find_waypoint_with_instance. No-op unless +// *cfg.cur_waypoint is currently non-null and the object is a valid +// OBJ_WAYPOINT. Called internally on the final leg of the waypoint-list +// operations below; public for callers whose own mutations (e.g. waypoint +// creation or deletion) can also invalidate the pointers. +void rederive_cur_waypoint(const FredWaypointConfig& cfg); + +// Swap the waypoint lists at indices a and b within Waypoint_lists. Also +// resorts obj_used_list and re-derives the cfg's current-waypoint pointers. +void swap_waypoint_lists(int a, int b, const FredWaypointConfig& cfg); + +// Move the waypoint list at index from_pos to to_pos within Waypoint_lists, +// shifting the lists in between by one and preserving their relative order. +// Waypoint_lists is kept compact, so positions are plain indices. Also +// resorts obj_used_list and re-derives the cfg's current-waypoint pointers. +void rotate_waypoint_lists(int from_pos, int to_pos, const FredWaypointConfig& cfg); + +// Move the waypoint at 0-based index `from` to index `to` within +// Waypoint_lists[list_index], shifting the waypoints in between by one. The +// waypoint positions shuffle while the objects stay in place, so obj_used_list +// and any waypoint pointers are unaffected; SEXP and AI goal references to the +// affected "listname:N" names are renamed to track the move. +void move_waypoint_within_list(int list_index, int from, int to); + +// Swap the waypoints at 0-based indices a and b within +// Waypoint_lists[list_index], with the same reference renaming. +void swap_waypoints_within_list(int list_index, int a, int b); + +struct FredJumpNodeSlotConfig +{ + // No back-references exist (the node links to its object via m_objnum, + // which travels with it, and Objects[].instance is unused for jump nodes). + // Reserved for future fields. +}; + +// Swap the jump nodes in Jump_nodes[] slots a and b. Both slots must be +// attached (GetSCPObjectNumber() >= 0). Also resorts obj_used_list so UI +// lists that walk it match. +void swap_jump_node_slots(int a, int b, const FredJumpNodeSlotConfig& cfg); + +// Move the jump node at display position from_pos to to_pos within `slots` +// (the attached Jump_nodes[] indices, in display order), shifting the nodes in +// between by one and preserving their relative order. Also resorts +// obj_used_list so UI lists that walk it match. +void rotate_jump_node_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredJumpNodeSlotConfig& cfg); diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 913aeff5543..d7e3648790b 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -929,6 +929,8 @@ add_file_folder("MissionEditor" missioneditor/sexp_tree_opf.h missioneditor/sexp_tree_actions.cpp missioneditor/sexp_tree_actions.h + missioneditor/slot_operations.cpp + missioneditor/slot_operations.h ) # MissionUI files @@ -1818,6 +1820,7 @@ add_file_folder("Ui" ) add_file_folder("Utils" + utils/array_utils.h utils/base64.cpp utils/base64.h utils/encoding.cpp diff --git a/code/utils/array_utils.h b/code/utils/array_utils.h new file mode 100644 index 00000000000..dde6b4ff283 --- /dev/null +++ b/code/utils/array_utils.h @@ -0,0 +1,164 @@ +#pragma once + +// General-purpose array/vector manipulation utilities. +// These are type-agnostic operations (insert, remove, move) that can be +// reused across different element types. + +#include +#include +#include + +#include "globalincs/pstypes.h" +#include "math/floating.h" + +// --------------------------------------------------------------------------- +// Raw array overloads (array + count + max_size) +// --------------------------------------------------------------------------- + +// Open a slot at `index` by shifting elements [index, count) right by one. +// Increments count. Returns false if the array is already full. +template +bool array_insert_slot(T *arr, int &count, int max_size, int index) +{ + Assertion(index >= 0 && index <= count, "array_insert_slot: index %d out of range [0, %d]", index, count); + if (count >= max_size) + return false; + for (int i = count; i > index; i--) + arr[i] = std::move(arr[i - 1]); + count++; + return true; +} + +// Close a slot at `index` by shifting elements (index, count) left by one. +// Decrements count. +template +void array_remove_slot(T *arr, int &count, int index) +{ + Assertion(index >= 0 && index < count, "array_remove_slot: index %d out of range [0, %d)", index, count); + for (int i = index; i < count - 1; i++) + arr[i] = std::move(arr[i + 1]); + count--; +} + +// Move element at `from` to `to`, shifting intermediate elements. +template +void array_move_element(T *arr, int count, int from, int to) +{ + Assertion(from >= 0 && from < count, "array_move_element: from %d out of range [0, %d)", from, count); + Assertion(to >= 0 && to < count, "array_move_element: to %d out of range [0, %d)", to, count); + if (from == to) + return; + T temp = std::move(arr[from]); + if (from < to) { + for (int i = from; i < to; i++) + arr[i] = std::move(arr[i + 1]); + } else { + for (int i = from; i > to; i--) + arr[i] = std::move(arr[i - 1]); + } + arr[to] = std::move(temp); +} + +// --------------------------------------------------------------------------- +// Swap-based raw array overloads (array + count + max_size) +// +// For element types that hold raw owning pointers (e.g. brief_stage's icons +// and lines buffers), the assignment-based helpers above would duplicate a +// pointer across two slots and orphan another buffer. These variants +// rearrange slots exclusively by swapping adjacent elements, so every slot +// keeps a distinct buffer. swap() is found via ADL, falling back to +// std::swap. +// --------------------------------------------------------------------------- + +// Open a slot at `index` by rotating elements [index, count] right by one. +// The slot at `index` receives the former one-past-end element, so the +// caller must reset its fields. Increments count. Returns false if the +// array is already full. +template +bool array_insert_slot_swap(T *arr, int &count, int max_size, int index) +{ + Assertion(index >= 0 && index <= count, "array_insert_slot_swap: index %d out of range [0, %d]", index, count); + if (count >= max_size) + return false; + using std::swap; + for (int i = count; i > index; i--) + swap(arr[i], arr[i - 1]); + count++; + return true; +} + +// Close the slot at `index` by rotating elements [index, count) left by one. +// The removed element is parked at the new one-past-end slot rather than +// destroyed. Decrements count. +template +void array_remove_slot_swap(T *arr, int &count, int index) +{ + Assertion(index >= 0 && index < count, "array_remove_slot_swap: index %d out of range [0, %d)", index, count); + using std::swap; + for (int i = index; i < count - 1; i++) + swap(arr[i], arr[i + 1]); + count--; +} + +// Move element at `from` to `to` by rotating the elements in between. +template +void array_move_element_swap(T *arr, int count, int from, int to) +{ + Assertion(from >= 0 && from < count, "array_move_element_swap: from %d out of range [0, %d)", from, count); + Assertion(to >= 0 && to < count, "array_move_element_swap: to %d out of range [0, %d)", to, count); + using std::swap; + if (from < to) { + for (int i = from; i < to; i++) + swap(arr[i], arr[i + 1]); + } else { + for (int i = from; i > to; i--) + swap(arr[i], arr[i - 1]); + } +} + +// --------------------------------------------------------------------------- +// Vector overloads +// --------------------------------------------------------------------------- + +// Open a slot at `index` by shifting elements [index, count) right by one. +// Grows the vector if needed. Increments count. +template +void array_insert_slot(SCP_vector &vec, int &count, int index) +{ + Assertion(index >= 0 && index <= count, "array_insert_slot: index %d out of range [0, %d]", index, count); + if (count >= sz2i(vec.size())) + vec.resize(count + 1); + for (int i = count; i > index; i--) + vec[i] = std::move(vec[i - 1]); + count++; +} + +// Close a slot at `index` by shifting elements (index, count) left by one. +// Decrements count. Does not shrink the vector. +template +void array_remove_slot(SCP_vector &vec, int &count, int index) +{ + Assertion(index >= 0 && index < count, "array_remove_slot: index %d out of range [0, %d)", index, count); + for (int i = index; i < count - 1; i++) + vec[i] = std::move(vec[i + 1]); + count--; +} + +// Move element at `from` to `to`, shifting intermediate elements. +template +void array_move_element(SCP_vector &vec, int from, int to) +{ + Assertion(vec.in_bounds(from), "array_move_element: from %d out of range [0, " SIZE_T_ARG ")", from, vec.size()); + Assertion(vec.in_bounds(to), "array_move_element: to %d out of range [0, " SIZE_T_ARG ")", to, vec.size()); + if (from == to) + return; + T temp = std::move(vec[from]); + if (from < to) { + for (int i = from; i < to; i++) + vec[i] = std::move(vec[i + 1]); + } else { + for (int i = from; i > to; i--) + vec[i] = std::move(vec[i - 1]); + } + vec[to] = std::move(temp); +} diff --git a/fred2/management.cpp b/fred2/management.cpp index f0e65be50da..0fc80d32dad 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -1980,19 +1980,6 @@ int get_ship_from_obj(object *objp) return 0; } -void ai_update_goal_references(sexp_ref_type type, const char *old_name, const char *new_name) -{ - int i; - - for (i=0; i query_referenced_in_ai_goals(sexp_ref_type type, const char *name) { int i; diff --git a/fred2/management.h b/fred2/management.h index c99cde1c0bc..e9b1f34e291 100644 --- a/fred2/management.h +++ b/fred2/management.h @@ -105,7 +105,6 @@ void correct_marking(); int get_ship_from_obj(int obj); int get_ship_from_obj(object* objp); void set_valid_dock_points(int ship, int type, CComboBox* box); -void ai_update_goal_references(sexp_ref_type type, const char* old_name, const char* new_name); std::pair query_referenced_in_ai_goals(sexp_ref_type type, const char* name); int advanced_stricmp(char* one, char* two); int reference_handler(const char* name, sexp_ref_type type, int obj); diff --git a/fred2/reorderdlg.cpp b/fred2/reorderdlg.cpp index 814d38d0865..4e4e82d6fde 100644 --- a/fred2/reorderdlg.cpp +++ b/fred2/reorderdlg.cpp @@ -7,7 +7,7 @@ #include "management.h" #include "reorderdlg.h" -#include "missioneditor/common.h" +#include "missioneditor/slot_operations.h" #include "ship/ship.h" #ifdef _DEBUG diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index b4f147c9d1e..86f9864d485 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -1413,19 +1413,7 @@ int Editor::invalidate_references(const char* name, sexp_ref_type type) { return 0; } void Editor::ai_update_goal_references(sexp_ref_type type, const char* old_name, const char* new_name) { - int i; - - for (i = 0; i < MAX_AI_INFO; i++) { // loop through all Ai_info entries - if (Ai_info[i].shipnum != -1) { // skip if unused - ::ai_update_goal_references(Ai_info[i].goals, type, old_name, new_name); - } - } - - for (i = 0; i < MAX_WINGS; i++) { - if (Wings[i].wave_count) { - ::ai_update_goal_references(Wings[i].ai_goals, type, old_name, new_name); - } - } + ::ai_update_goal_references(type, old_name, new_name); } void Editor::update_texture_replacements(const char* old_name, const char* new_name) { for (SCP_vector::iterator ii = Fred_texture_replacements.begin(); diff --git a/qtfred/src/mission/dialogs/ReorderDialogModel.cpp b/qtfred/src/mission/dialogs/ReorderDialogModel.cpp index 9662491fd8b..e4beec8bafa 100644 --- a/qtfred/src/mission/dialogs/ReorderDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ReorderDialogModel.cpp @@ -1,6 +1,6 @@ #include "mission/dialogs/ReorderDialogModel.h" -#include "missioneditor/common.h" +#include "missioneditor/slot_operations.h" #include "jumpnode/jumpnode.h" #include "object/waypoint.h" #include "prop/prop.h" @@ -111,16 +111,25 @@ void ReorderDialogModel::moveItem(Type type, int from_pos, int to_pos) rotate_wing_slots(slotList, from_pos, to_pos, cfg); break; } - case Type::Props: - rotate_prop_slots(slotList, from_pos, to_pos); + case Type::Props: { + FredPropSlotConfig cfg; + rotate_prop_slots(slotList, from_pos, to_pos, cfg); break; - case Type::WaypointLists: - rotate_waypoint_lists(from_pos, to_pos); + } + case Type::WaypointLists: { + FredWaypointConfig cfg; + cfg.cur_waypoint = &_editor->cur_waypoint; + cfg.cur_waypoint_list = &_editor->cur_waypoint_list; + cfg.cur_object_index = &_editor->currentObject; + rotate_waypoint_lists(from_pos, to_pos, cfg); break; - case Type::JumpNodes: - rotate_jump_nodes(slotList, from_pos, to_pos); + } + case Type::JumpNodes: { + FredJumpNodeSlotConfig cfg; + rotate_jump_node_slots(slotList, from_pos, to_pos, cfg); break; } + } set_modified(); _editor->missionChanged();