From 9bab7b323a2ef1783617e36b06e07cce8f6465b4 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 30 May 2026 02:38:32 -0400 Subject: [PATCH 1/3] add Ships[] slot reassignment utilities for FRED/qtFRED reassign_ship_slot moves a ship between Ships[] slots, fixing up every back-reference: Objects[].instance, Ai_info[].shipnum, Wings[].ship_index[], Player_start_shipnum, Ship_registry's cached shipnum, and the FRED-side parallel arrays Fred_alt_names/Fred_callsigns plus cur_ship (passed via FredShipSlotConfig so non-FRED callers can opt out). swap_ship_slots wraps it as a three-leg swap through a temporary empty slot. Co-Authored-By: Claude Opus 4.7 (1M context) --- code/missioneditor/common.cpp | 211 ++++++++++++++++++++++++++++++++++ code/missioneditor/common.h | 29 +++++ 2 files changed, 240 insertions(+) diff --git a/code/missioneditor/common.cpp b/code/missioneditor/common.cpp index 17ece9e321e..937bf68fcd7 100644 --- a/code/missioneditor/common.cpp +++ b/code/missioneditor/common.cpp @@ -1,9 +1,14 @@ // methods and members common to any mission editor FSO may have #include "common.h" +#include "ai/ai.h" +#include "globalincs/linklist.h" #include "mission/missionparse.h" #include "iff_defs/iff_defs.h" +#include "object/object.h" #include "ship/ship.h" +#include + // to keep track of data char Voice_abbrev_briefing[NAME_LENGTH]; char Voice_abbrev_campaign[NAME_LENGTH]; @@ -145,3 +150,209 @@ void generate_weaponry_usage_list_wing(int wing_num, int* arr) } } } + +void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list) +{ + 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. Per the engine's convention, a slot with + // objnum < 0 is considered empty; other fields in the vacated slot are + // left as-is (unreachable through the "is this slot used" guard). + // Move (not copy) because ship contains a unique_ptr member. + Ships[to] = std::move(Ships[from]); + Ships[from].objnum = -1; + + // subsys_list is an intrusive doubly-linked list whose head sentinel's + // address is meaningful: real nodes' prev/next bookend back to &head, and + // an empty list is self-referential. The move copied those pointers + // verbatim, so they still reference the old (vacated) sentinel address. + { + auto old_head = &Ships[from].subsys_list; + auto new_head = &Ships[to].subsys_list; + if (new_head->next == old_head) + { + // Empty list: re-init self-referential on the new head. + new_head->next = new_head; + new_head->prev = new_head; + } + else + { + // Non-empty: repoint the first node's prev and the last node's next. + new_head->next->prev = new_head; + new_head->prev->next = new_head; + } + } + + // 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). + 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 find_free_slot(int max_slots, bool (*slot_is_empty)(int), const char* caller) +{ + for (int i = 0; i < max_slots; ++i) + { + if (slot_is_empty(i)) + return i; + } + Assertion(false, "%s: no free slot available for the temporary leg", caller); + 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, caller); + + // 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 = (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, caller); + + // 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"); +} + +// 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; }); +} diff --git a/code/missioneditor/common.h b/code/missioneditor/common.h index 085e7f24c19..0d4b236e51b 100644 --- a/code/missioneditor/common.h +++ b/code/missioneditor/common.h @@ -48,3 +48,32 @@ anchor_t target_to_anchor(int target); void generate_weaponry_usage_list_team(int team, int* arr); void generate_weaponry_usage_list_wing(int wing_num, int* arr); + +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); + +// 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(); From 4951673e11b37b7fbe36ce58c63bb716adc21761 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 30 May 2026 14:21:27 -0400 Subject: [PATCH 2/3] add Wings[] slot reassignment utilities for FRED/qtFRED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reassign_wing_slot moves a wing between Wings[] slots, fixing up every back-reference: Ships[i].wingnum, the Starting/Squadron/TVT_wings caches (via update_custom_wing_indexes), the FRED-side parallel array wing_objects, and cur_wing (passed via FredWingSlotConfig so non-FRED callers can opt out). swap_wing_slots wraps it as a three-leg swap through a temporary empty slot. Also consolidates update_custom_wing_indexes (previously duplicated verbatim in fred2/management.cpp and qtfred Editor) into common.cpp so the new utility — and any future caller — has one shared implementation. Co-Authored-By: Claude Opus 4.7 (1M context) --- code/missioneditor/common.cpp | 71 +++++++++++++++++++ code/missioneditor/common.h | 28 ++++++++ fred2/management.cpp | 23 ------ fred2/management.h | 1 - qtfred/src/mission/Editor.h | 5 -- qtfred/src/mission/EditorWing.cpp | 17 +---- .../dialogs/MissionSpecDialogModel.cpp | 3 +- 7 files changed, 102 insertions(+), 46 deletions(-) diff --git a/code/missioneditor/common.cpp b/code/missioneditor/common.cpp index 937bf68fcd7..ac6af11e6f1 100644 --- a/code/missioneditor/common.cpp +++ b/code/missioneditor/common.cpp @@ -97,6 +97,20 @@ anchor_t target_to_anchor(int target) 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_weaponry_usage_list_team(int team, int* arr) { int i; @@ -245,6 +259,11 @@ 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), const char* caller) { for (int i = 0; i < max_slots; ++i) @@ -314,6 +333,58 @@ void rotate_ship_slots(const SCP_vector& slots, int from_pos, int to_pos, c 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(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 diff --git a/code/missioneditor/common.h b/code/missioneditor/common.h index 0d4b236e51b..27135e1815a 100644 --- a/code/missioneditor/common.h +++ b/code/missioneditor/common.h @@ -45,6 +45,11 @@ int anchor_to_target(anchor_t anchor); anchor_t target_to_anchor(int target); +// Rebuild Starting_wings[], Squadron_wings[], TVT_wings[] from their parallel +// name arrays via wing_name_lookup. Consolidated from FRED's and qtFRED's +// previously-duplicated copies. +void update_custom_wing_indexes(); + void generate_weaponry_usage_list_team(int team, int* arr); void generate_weaponry_usage_list_wing(int wing_num, int* arr); @@ -74,6 +79,29 @@ void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg); // 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(); diff --git a/fred2/management.cpp b/fred2/management.cpp index 37a59dd75dc..56a1d56cfba 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -2625,29 +2625,6 @@ int wing_is_player_wing(int wing) return 0; } -// Goober5000 -// This must be done when either the wing name or the custom name is changed. -// (It's also duplicated in FS2, in post_process_mission, for setting the indexes at mission load.) -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); - } -} - // Goober5000 void update_texture_replacements(const char *old_name, const char *new_name) { diff --git a/fred2/management.h b/fred2/management.h index 7d76ff585ee..ffa5725d519 100644 --- a/fred2/management.h +++ b/fred2/management.h @@ -131,7 +131,6 @@ extern void management_add_ships_to_combo(CComboBox* box, int flags); // Goober5000 extern int wing_is_player_wing(int wing); -extern void update_custom_wing_indexes(); extern void update_texture_replacements(const char* old_name, const char* new_name); #endif diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index 0fe7ce21ee1..b5e96cb4b5a 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -188,11 +188,6 @@ class Editor : public QObject { subsys_to_render Render_subsys; - // Goober5000 - // This must be done when either the wing name or the custom name is changed. - // (It's also duplicated in FS2, in post_process_mission, for setting the indexes at mission load.) - static void update_custom_wing_indexes(); - void ai_update_goal_references(sexp_ref_type type, const char* old_name, const char* new_name); // Goober5000 diff --git a/qtfred/src/mission/EditorWing.cpp b/qtfred/src/mission/EditorWing.cpp index 2612374008c..6d8d2387bbe 100644 --- a/qtfred/src/mission/EditorWing.cpp +++ b/qtfred/src/mission/EditorWing.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace { @@ -69,22 +70,6 @@ void Editor::set_cur_wing(int wing) updateAllViewports(); // TODO: Add notification for a changed selection } -void Editor::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); - } -} int Editor::create_wing() { diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp index 787747cff04..3ac8c71527b 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp @@ -12,6 +12,7 @@ #include "cfile/cfile.h" #include "localization/localize.h" +#include "missioneditor/common.h" #include "mission/missionmessage.h" #include "mission/mission_flags.h" #include "scripting/global_hooks.h" @@ -227,7 +228,7 @@ bool MissionSpecDialogModel::apply() { strcpy_s(TVT_wing_names[i], _m_custom_tvt_wings[i].c_str()); } - Editor::update_custom_wing_indexes(); + update_custom_wing_indexes(); // scripts may rebuild LuaEnums when custom data/strings change. if (scripting::hooks::FredOnMissionSpecsSave->isActive()) { From 78b2b93cc07fb8d6a2b7991b5fa095f395a0cd2a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 10 Jun 2026 14:50:04 -0400 Subject: [PATCH 3/3] add a Reorder Ships and Wings editor to FRED Accessible from the Tools menu, this dialog lists all ships or all wings (selectable via a drop-down) and provides Move to top/up/down/ bottom buttons. Moves are applied immediately via the slot reassignment utilities in missioneditor/common.h, using adjacent swaps so the relative order of other entries is preserved. Co-Authored-By: Claude Fable 5 --- fred2/CMakeLists.txt | 2 + fred2/fred.rc | 23 ++++++ fred2/fredview.cpp | 9 +++ fred2/fredview.h | 1 + fred2/reorderdlg.cpp | 176 +++++++++++++++++++++++++++++++++++++++++++ fred2/reorderdlg.h | 40 ++++++++++ fred2/resource.h | 30 +++++--- 7 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 fred2/reorderdlg.cpp create mode 100644 fred2/reorderdlg.h diff --git a/fred2/CMakeLists.txt b/fred2/CMakeLists.txt index 14ba24d137f..795cd4ab55e 100644 --- a/fred2/CMakeLists.txt +++ b/fred2/CMakeLists.txt @@ -105,6 +105,8 @@ set(FRED2_SOURCES propdlg.h reinforcementeditordlg.cpp reinforcementeditordlg.h + reorderdlg.cpp + reorderdlg.h resource.h restrictpaths.cpp restrictpaths.h diff --git a/fred2/fred.rc b/fred2/fred.rc index c9c5835a172..f7fc1bd73a7 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -404,6 +404,7 @@ BEGIN BEGIN MENUITEM "&Voice Acting Manager", ID_EDITORS_VOICE MENUITEM "Set Global Ship Flags", 33073 + MENUITEM "Reorder Ships and Wings", ID_REORDER MENUITEM "Calculate Relative Coordinates", 33030 MENUITEM "Music Player", ID_MUSIC_PLAYER MENUITEM "Mission Statistics\tCtrl+Shift+D", 33067 @@ -2030,6 +2031,19 @@ BEGIN EDITTEXT IDC_ORIENTATION_H,255,81,49,14,ES_READONLY END +IDD_REORDER DIALOG 0, 0, 185, 191 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Reorder Ships and Wings" +FONT 8, "MS Sans Serif" +BEGIN + COMBOBOX IDC_REORDER_TYPE,7,7,98,114,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LISTBOX IDC_REORDER_LIST,7,25,98,159,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Move to top",IDC_REORDER_MOVE_TO_TOP,112,25,66,14 + PUSHBUTTON "Move up",IDC_REORDER_MOVE_UP,112,43,66,14 + PUSHBUTTON "Move down",IDC_REORDER_MOVE_DOWN,112,61,66,14 + PUSHBUTTON "Move to bottom",IDC_REORDER_MOVE_TO_BOTTOM,112,79,66,14 +END + IDD_SHIELD_SYS DIALOG 0, 0, 192, 106 STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Shield System Editor" @@ -2929,6 +2943,14 @@ BEGIN BOTTOMMARGIN, 87 END + IDD_REORDER, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 178 + TOPMARGIN, 7 + BOTTOMMARGIN, 184 + END + IDD_SHIELD_SYS, DIALOG BEGIN LEFTMARGIN, 7 @@ -3791,6 +3813,7 @@ END STRINGTABLE BEGIN ID_CALC_RELATIVE_COORDS "Calculate orientation and distance relative to an origin object" + ID_REORDER "Change the order in which ships and wings are stored in the mission" ID_EDITORS_ADJUST_GRID "Adjust the orientation and level of the grid" ID_EDITORS_SHIELD_SYS "Editor for availability of shield system for ships" ID_ALIGN_OBJ "Align object (or camera if no objects are marked) X/Y/Z axes with the global axes that are closest" diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index abf87a61e8f..eed02786415 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -61,6 +61,7 @@ #include "sound/audiostr.h" #include "mission/missiongrid.h" #include "calcrelativecoordsdlg.h" +#include "reorderdlg.h" #include "musicplayerdlg.h" #include "volumetricsdlg.h" #include "customdatadlg.h" @@ -341,6 +342,7 @@ BEGIN_MESSAGE_MAP(CFREDView, CView) ON_UPDATE_COMMAND_UI(ID_LOOKAT_OBJ, OnUpdateLookatObj) ON_COMMAND(ID_EDITORS_ADJUST_GRID, OnEditorsAdjustGrid) ON_COMMAND(ID_CALC_RELATIVE_COORDS, OnCalcRelativeCoords) + ON_COMMAND(ID_REORDER, OnReorder) ON_COMMAND(ID_MUSIC_PLAYER, OnMusicPlayer) ON_COMMAND(ID_EDITORS_SHIELD_SYS, OnEditorsShieldSys) ON_COMMAND(ID_LEVEL_OBJ, OnLevelObj) @@ -4474,6 +4476,13 @@ void CFREDView::OnCalcRelativeCoords() dlg.DoModal(); } +void CFREDView::OnReorder() +{ + reorder_dlg dlg; + + dlg.DoModal(); +} + void CFREDView::OnMusicPlayer() { Assertion(Music_player_dialog.GetSafeHwnd(), "Unable to create music player window!"); diff --git a/fred2/fredview.h b/fred2/fredview.h index badc2585324..36520dbf539 100644 --- a/fred2/fredview.h +++ b/fred2/fredview.h @@ -301,6 +301,7 @@ class CFREDView : public CView afx_msg void OnUpdateLookatObj(CCmdUI* pCmdUI); afx_msg void OnEditorsAdjustGrid(); afx_msg void OnCalcRelativeCoords(); + afx_msg void OnReorder(); afx_msg void OnMusicPlayer(); afx_msg void OnEditorsShieldSys(); afx_msg void OnLevelObj(); diff --git a/fred2/reorderdlg.cpp b/fred2/reorderdlg.cpp new file mode 100644 index 00000000000..814d38d0865 --- /dev/null +++ b/fred2/reorderdlg.cpp @@ -0,0 +1,176 @@ +// reorderdlg.cpp : implementation file +// + +#include "stdafx.h" +#include "FRED.h" +#include "freddoc.h" +#include "management.h" +#include "reorderdlg.h" + +#include "missioneditor/common.h" +#include "ship/ship.h" + +#ifdef _DEBUG +#undef THIS_FILE +static char THIS_FILE[] = __FILE__; +#endif + +reorder_dlg::reorder_dlg(CWnd *pParent /*=nullptr*/) + : CDialog(reorder_dlg::IDD, pParent) +{ + //{{AFX_DATA_INIT(reorder_dlg) + //}}AFX_DATA_INIT +} + +void reorder_dlg::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(reorder_dlg) + DDX_Control(pDX, IDC_REORDER_TYPE, m_type_combo); + DDX_Control(pDX, IDC_REORDER_LIST, m_list); + //}}AFX_DATA_MAP +} + +BEGIN_MESSAGE_MAP(reorder_dlg, CDialog) + //{{AFX_MSG_MAP(reorder_dlg) + ON_CBN_SELCHANGE(IDC_REORDER_TYPE, OnSelchangeReorderType) + ON_LBN_SELCHANGE(IDC_REORDER_LIST, OnSelchangeReorderList) + ON_BN_CLICKED(IDC_REORDER_MOVE_TO_TOP, OnMoveToTop) + ON_BN_CLICKED(IDC_REORDER_MOVE_UP, OnMoveUp) + ON_BN_CLICKED(IDC_REORDER_MOVE_DOWN, OnMoveDown) + ON_BN_CLICKED(IDC_REORDER_MOVE_TO_BOTTOM, OnMoveToBottom) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +BOOL reorder_dlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + m_type_combo.AddString("Ships"); + m_type_combo.AddString("Wings"); + m_type_combo.SetCurSel(0); + + populate_list(); + update_buttons(); + + return TRUE; +} + +bool reorder_dlg::ships_selected() const +{ + return m_type_combo.GetCurSel() == 0; +} + +void reorder_dlg::populate_list() +{ + m_list.ResetContent(); + m_slots.clear(); + + if (ships_selected()) + { + for (int i = 0; i < MAX_SHIPS; ++i) + { + if (Ships[i].objnum >= 0) + { + m_list.AddString(Ships[i].ship_name); + m_slots.push_back(i); + } + } + } + else + { + for (int i = 0; i < MAX_WINGS; ++i) + { + if (Wings[i].wave_count > 0) + { + m_list.AddString(Wings[i].name); + m_slots.push_back(i); + } + } + } +} + +void reorder_dlg::update_buttons() +{ + int pos = m_list.GetCurSel(); + int count = (int)m_slots.size(); + bool can_move_up = (pos > 0); + bool can_move_down = (pos >= 0 && pos < count - 1); + + GetDlgItem(IDC_REORDER_MOVE_TO_TOP)->EnableWindow(can_move_up); + GetDlgItem(IDC_REORDER_MOVE_UP)->EnableWindow(can_move_up); + GetDlgItem(IDC_REORDER_MOVE_DOWN)->EnableWindow(can_move_down); + GetDlgItem(IDC_REORDER_MOVE_TO_BOTTOM)->EnableWindow(can_move_down); +} + +void reorder_dlg::move_selected(bool up, bool all_the_way) +{ + int pos = m_list.GetCurSel(); + int count = (int)m_slots.size(); + if (pos < 0 || pos >= count) + return; + + int target; + if (up) + target = all_the_way ? 0 : pos - 1; + else + target = all_the_way ? count - 1 : pos + 1; + + if (target < 0 || target >= count || target == pos) + return; + + // Rotate the item to the target position, which preserves the relative + // order of everything else in the list. + if (ships_selected()) + { + FredShipSlotConfig cfg; + cfg.fred_alt_names = Fred_alt_names; + cfg.fred_callsigns = Fred_callsigns; + cfg.cur_ship = &cur_ship; + rotate_ship_slots(m_slots, pos, target, cfg); + } + else + { + FredWingSlotConfig cfg; + cfg.wing_objects = wing_objects; + cfg.cur_wing = &cur_wing; + rotate_wing_slots(m_slots, pos, target, cfg); + } + + set_modified(); + + populate_list(); + m_list.SetCurSel(target); + update_buttons(); +} + +void reorder_dlg::OnSelchangeReorderType() +{ + populate_list(); + update_buttons(); +} + +void reorder_dlg::OnSelchangeReorderList() +{ + update_buttons(); +} + +void reorder_dlg::OnMoveToTop() +{ + move_selected(true, true); +} + +void reorder_dlg::OnMoveUp() +{ + move_selected(true, false); +} + +void reorder_dlg::OnMoveDown() +{ + move_selected(false, false); +} + +void reorder_dlg::OnMoveToBottom() +{ + move_selected(false, true); +} diff --git a/fred2/reorderdlg.h b/fred2/reorderdlg.h new file mode 100644 index 00000000000..c67778c3951 --- /dev/null +++ b/fred2/reorderdlg.h @@ -0,0 +1,40 @@ +// reorderdlg.h : header file + +#ifndef _REORDERDLG_H +#define _REORDERDLG_H + +class reorder_dlg : public CDialog +{ +public: + reorder_dlg(CWnd *pParent = nullptr); + + // Dialog Data + //{{AFX_DATA(reorder_dlg) + enum { IDD = IDD_REORDER }; + CComboBox m_type_combo; + CListBox m_list; + //}}AFX_DATA + +protected: + virtual void DoDataExchange(CDataExchange *pDX); // DDX/DDV support + + //{{AFX_MSG(reorder_dlg) + virtual BOOL OnInitDialog(); + afx_msg void OnSelchangeReorderType(); + afx_msg void OnSelchangeReorderList(); + afx_msg void OnMoveToTop(); + afx_msg void OnMoveUp(); + afx_msg void OnMoveDown(); + afx_msg void OnMoveToBottom(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() + + bool ships_selected() const; + void populate_list(); + void update_buttons(); + void move_selected(bool up, bool all_the_way); + + // occupied Ships[] or Wings[] slot indexes, in list order + SCP_vector m_slots; +}; +#endif // _REORDERDLG_H diff --git a/fred2/resource.h b/fred2/resource.h index 35e6ed4f750..13f06b05355 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -138,6 +138,7 @@ #define IDD_VOLUMETRICS 332 #define IDD_EDIT_CUSTOM_STRINGS 333 #define IDD_SUPPORT_REARM_OPTIONS 334 +#define IDD_REORDER 335 #define IDC_SHIP_CLASS 1003 #define IDC_SHIP_WING 1004 #define IDC_SOUND_CLIP_NAME 1007 @@ -1290,6 +1291,12 @@ #define IDC_SUPPORT_REARM_SET_ALL_UNLIMITED 1729 #define IDC_SUPPORT_REARM_SET_ALL_ZERO 1730 #define IDC_SUPPORT_REARM_POOL_TEAM 1731 +#define IDC_REORDER_TYPE 1732 +#define IDC_REORDER_LIST 1733 +#define IDC_REORDER_MOVE_TO_TOP 1734 +#define IDC_REORDER_MOVE_UP 1735 +#define IDC_REORDER_MOVE_DOWN 1736 +#define IDC_REORDER_MOVE_TO_BOTTOM 1737 #define IDC_SEXP_POPUP_LIST 32770 #define ID_FILE_MISSIONNOTES 32771 #define ID_DUPLICATE 32774 @@ -1471,14 +1478,6 @@ #define ID_EDITORS_WAYPOINT 32979 #define ID_VIEW_OUTLINES 32980 #define ID_NEW_SHIP_TYPE 32981 -#define ID_NEW_PROP_TYPE 33104 -#define ID_STATIC_SHIP_LABEL 33105 -#define ID_STATIC_PROP_LABEL 33106 -#define ID_OUTLINE_LOD_0 33107 -#define ID_OUTLINE_LOD_1 33108 -#define ID_OUTLINE_LOD_2 33109 -#define ID_OUTLINE_LOD_3 33110 -#define ID_OUTLINE_LOD_4 33111 #define ID_VIEW_OUTLINES_ON_SELECTED 32982 #define ID_SHOW_STARFIELD 32983 #define ID_ASTEROID_EDITOR 32984 @@ -1593,6 +1592,15 @@ #define ID_MISC_POINTUSINGUVEC 33101 #define ID_MUSIC_PLAYER 33102 #define ID_EDITORS_VOLUMETRICS 33103 +#define ID_NEW_PROP_TYPE 33104 +#define ID_STATIC_SHIP_LABEL 33105 +#define ID_STATIC_PROP_LABEL 33106 +#define ID_OUTLINE_LOD_0 33107 +#define ID_OUTLINE_LOD_1 33108 +#define ID_OUTLINE_LOD_2 33109 +#define ID_OUTLINE_LOD_3 33110 +#define ID_OUTLINE_LOD_4 33111 +#define ID_REORDER 33112 #define ID_INDICATOR_MODE 59142 #define ID_INDICATOR_LEFT 59143 #define ID_INDICATOR_RIGHT 59144 @@ -1604,9 +1612,9 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_3D_CONTROLS 1 -#define _APS_NEXT_RESOURCE_VALUE 335 -#define _APS_NEXT_COMMAND_VALUE 33112 -#define _APS_NEXT_CONTROL_VALUE 1732 +#define _APS_NEXT_RESOURCE_VALUE 336 +#define _APS_NEXT_COMMAND_VALUE 33113 +#define _APS_NEXT_CONTROL_VALUE 1738 #define _APS_NEXT_SYMED_VALUE 105 #endif #endif