From b774d9b99b64c8c2219a1c17a21c37ad48162423 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 16 Aug 2026 21:21:25 -0400 Subject: [PATCH] preserve custom player wing arrival cues (#7160) FRED and qtFRED no longer overwrite a hand-edited arrival cue on the wing containing the player start. The cue is editable when the wing has multiple waves (where it governs the arrival of subsequent waves) and read-only otherwise. Waves and threshold are now editable for the single-player player wing, while the arrival delay remains locked; the arrival location is editable in both editors. Both error checkers flag a single-wave player wing with a custom cue as a potential issue, and the engine now warns and aborts the mission load, rather than tripping an Assert, if the player start ship was not created at mission start. Also fix an unguarded read of Ships[Player_start_shipnum] in both editors. Co-Authored-By: Claude Fable 5 --- code/mission/missionparse.cpp | 6 ++- fred2/fredview.cpp | 29 ++++++++++-- fred2/wing_editor.cpp | 55 +++++++++++++++------- fred2/wing_editor.h | 1 + qtfred/src/mission/EditorWing.cpp | 2 +- qtfred/src/ui/dialogs/WingEditorDialog.cpp | 46 +++++++++--------- qtfred/src/ui/util/ErrorChecker.cpp | 20 ++++++++ 7 files changed, 115 insertions(+), 44 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 301a229abf9..a66d2cae2e9 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -7015,7 +7015,11 @@ bool post_process_mission(mission *pm) return false; } Player_start_shipnum = player_start_entry->shipnum; - Assert( Player_start_shipnum != -1 ); + if (Player_start_shipnum < 0) { + // the ship exists in the mission file but its first wave was never created + Warning(LOCATION, "Player start ship '%s' was not created at mission start! If the wing containing the player has a custom arrival cue, the cue must evaluate to true at the beginning of the mission. Similarly, if the wing arrives from a hangar bay, the mothership must be present at the beginning of the mission.", Player_start_shipname); + return false; + } Player_start_pobject = player_start_entry->p_objp(); Assert( Player_start_pobject != NULL ); diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index 3898169d687..552d7725911 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -3301,9 +3301,32 @@ int CFREDView::global_error_check_player_wings(int multi) } } -// // player's wing must have a true arrival -// free_sexp2(Wings[z].arrival_cue); -// Wings[z].arrival_cue = Locked_sexp_true; + // The wing containing the player start must be present at mission start, so a custom arrival cue + // is only useful for controlling the arrival of subsequent waves. Flag a single-wave wing with a + // custom cue as a potential issue, but never change the cue itself. + if (Error_checker_checks_potential_issues || Error_checker_checks_potential_issues_once) + { + for (i=0; i 1) + continue; + if (Wings[i].arrival_cue == Locked_sexp_true) + continue; + + bool contains_player_start = false; + for (z=0; zMessageBox(issue_buf.c_str(), "Warning", MB_OKCANCEL | MB_ICONINFORMATION) != IDOK) + return 1; + } + } + } // Check to be sure that any player wing doesn't have > 1 wave for multiplayer if ( multi ) diff --git a/fred2/wing_editor.cpp b/fred2/wing_editor.cpp index 6d2921ae0b5..21f1005f30a 100644 --- a/fred2/wing_editor.cpp +++ b/fred2/wing_editor.cpp @@ -181,6 +181,7 @@ BEGIN_MESSAGE_MAP(wing_editor, CDialog) ON_BN_CLICKED(IDC_CUSTOM_WARPOUT_PARAMS, OnBnClickedCustomWarpoutParams) ON_BN_CLICKED(IDC_WING_FORMATION_ALIGN, OnWingFormationAlign) ON_EN_CHANGE(IDC_WING_NAME, OnChangeWingName) + ON_EN_CHANGE(IDC_WING_WAVES, OnChangeWingWaves) //}}AFX_MSG_MAP END_MESSAGE_MAP() @@ -273,7 +274,7 @@ void wing_editor::OnClose() // initialize everything that update_data_safe() saves. void wing_editor::initialize_data_safe(int full_update) { - int i, enable = TRUE, player_wing = 0, player_enabled = 1; + int i, enable = TRUE, player_wing = 0, player_enabled = 1, waves_enabled = 1; CComboBox *arrival_box, *departure_box; nprintf(("Fred routing", "Wing dialog load safe\n")); @@ -314,11 +315,15 @@ void wing_editor::initialize_data_safe(int full_update) m_same_arrival_warp_when_docked = FALSE; m_same_departure_warp_when_docked = FALSE; m_no_dynamic = 0; - player_enabled = enable = FALSE; + waves_enabled = player_enabled = enable = FALSE; } else { CComboBox *ptr; + if ((Player_start_shipnum >= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0)) + if (Ships[Player_start_shipnum].wingnum == cur_wing) + player_wing = 1; + if (The_mission.game_type & MISSION_TYPE_MULTI) { if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) @@ -326,7 +331,7 @@ void wing_editor::initialize_data_safe(int full_update) for (i=0; i= 0) && (Player_start_shipnum < MAX_SHIPS) && (Ships[Player_start_shipnum].objnum >= 0)) - if (Ships[Player_start_shipnum].wingnum == cur_wing) - player_wing = 1; - m_wing_squad_filename = _T(Wings[cur_wing].wing_squad_filename); m_wing_display_name = Wings[cur_wing].has_display_name() ? Wings[cur_wing].get_display_name() : ""; m_special_ship = Wings[cur_wing].special_ship; @@ -410,10 +412,7 @@ void wing_editor::initialize_data_safe(int full_update) m_departure_target = departure_box->FindStringExact(-1, Ships[m_departure_target].ship_name); m_departure_delay = Wings[cur_wing].departure_delay; - if (player_wing) - m_arrival_tree.load_tree(Locked_sexp_true); - else - m_arrival_tree.load_tree(Wings[cur_wing].arrival_cue); + m_arrival_tree.load_tree(Wings[cur_wing].arrival_cue); m_departure_tree.load_tree(Wings[cur_wing].departure_cue, "false"); m_hotkey = Wings[cur_wing].hotkey+1; @@ -452,11 +451,11 @@ void wing_editor::initialize_data_safe(int full_update) GetDlgItem(IDC_WING_NAME)->EnableWindow(enable); GetDlgItem(IDC_WING_SQUAD_LOGO_BUTTON)->EnableWindow(enable); GetDlgItem(IDC_WING_SPECIAL_SHIP)->EnableWindow(enable); - GetDlgItem(IDC_WING_WAVES)->EnableWindow(player_enabled); - GetDlgItem(IDC_WING_WAVE_THRESHOLD)->EnableWindow(player_enabled); + GetDlgItem(IDC_WING_WAVES)->EnableWindow(waves_enabled); + GetDlgItem(IDC_WING_WAVE_THRESHOLD)->EnableWindow(waves_enabled); GetDlgItem(IDC_DISBAND_WING)->EnableWindow(enable); - GetDlgItem(IDC_SPIN_WAVES)->EnableWindow(player_enabled); - GetDlgItem(IDC_SPIN_WAVE_THRESHOLD)->EnableWindow(player_enabled); + GetDlgItem(IDC_SPIN_WAVES)->EnableWindow(waves_enabled); + GetDlgItem(IDC_SPIN_WAVE_THRESHOLD)->EnableWindow(waves_enabled); GetDlgItem(IDC_WING_FORMATION)->EnableWindow(enable); GetDlgItem(IDC_WING_FORMATION_ALIGN)->EnableWindow(enable); @@ -496,8 +495,10 @@ void wing_editor::initialize_data_safe(int full_update) GetDlgItem(IDC_CUSTOM_WARPOUT_PARAMS)->EnableWindow(enable); } + // the player wing must be present at mission start, so its arrival cue is only editable + // if it has multiple waves (where the cue governs the arrival of subsequent waves) if (player_wing) - GetDlgItem(IDC_ARRIVAL_TREE)->EnableWindow(0); + GetDlgItem(IDC_ARRIVAL_TREE)->EnableWindow(m_waves > 1); else GetDlgItem(IDC_ARRIVAL_TREE)->EnableWindow(enable); @@ -1403,3 +1404,21 @@ void wing_editor::OnChangeWingName() // sync the variable to the edit box UpdateData(FALSE); } + +void wing_editor::OnChangeWingWaves() +{ + if (!GetSafeHwnd() || cur_wing < 0) + return; + + // only the player wing's arrival cue is gated on the number of waves + if ((Player_start_shipnum < 0) || (Player_start_shipnum >= MAX_SHIPS) || (Ships[Player_start_shipnum].objnum < 0)) + return; + if (Ships[Player_start_shipnum].wingnum != cur_wing) + return; + + // read the control directly; this notification can arrive in the middle of a DDX update, + // so calling UpdateData() here would not be safe + CString str; + GetDlgItem(IDC_WING_WAVES)->GetWindowText(str); + GetDlgItem(IDC_ARRIVAL_TREE)->EnableWindow(atoi(str) > 1); +} diff --git a/fred2/wing_editor.h b/fred2/wing_editor.h index 557be73eb6f..e9a304dbe77 100644 --- a/fred2/wing_editor.h +++ b/fred2/wing_editor.h @@ -117,6 +117,7 @@ class wing_editor : public CDialog afx_msg void OnBnClickedCustomWarpoutParams(); afx_msg void OnWingFormationAlign(); afx_msg void OnChangeWingName(); + afx_msg void OnChangeWingWaves(); //}}AFX_MSG DECLARE_MESSAGE_MAP() diff --git a/qtfred/src/mission/EditorWing.cpp b/qtfred/src/mission/EditorWing.cpp index a0ed64b6291..6b311b81a1d 100644 --- a/qtfred/src/mission/EditorWing.cpp +++ b/qtfred/src/mission/EditorWing.cpp @@ -452,7 +452,7 @@ bool Editor::wing_is_player_wing(int wing) } // Single player wing check } else { - if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS) { + if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS && Ships[Player_start_shipnum].objnum >= 0) { const int pw = Ships[Player_start_shipnum].wingnum; return pw >= 0 && pw == wing; } diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp index 394623ea17c..5bb423f4bf5 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -7,6 +7,7 @@ #include "ui_WingEditorDialog.h" #include +#include #include #include #include @@ -218,36 +219,39 @@ void WingEditorDialog::enableOrDisableControls() const bool containsPlayerStart = _model->containsPlayerStart(); const bool allFighterBombers = _model->wingAllFighterBombers(); - // Waves / Threshold: enabled only if NOT a player wing and all members are fighter/bombers - const bool wavesEnabled = (!isPlayerWing) && allFighterBombers; + // Waves / Threshold: locked for multiplayer starting/TVT wings, which must have exactly one + // wave; single-player wings, including the player wing, can have multiple waves. Also + // requires all members to be fighter/bombers. + const bool wavesEnabled = !(isPlayerWing && (The_mission.game_type & MISSION_TYPE_MULTI)) && allFighterBombers; ui->numWavesSpinBox->setEnabled(wavesEnabled); ui->waveThresholdSpinBox->setEnabled(wavesEnabled); - // Arrival section: disabled for starting wings (SP player wing or MP starting wing) - const bool arrivalEditable = !isPlayerWing; - ui->arrivalLocationCombo->setEnabled(arrivalEditable); - ui->arrivalDelaySpinBox->setEnabled(arrivalEditable); - ui->minDelaySpinBox->setEnabled(arrivalEditable); - ui->maxDelaySpinBox->setEnabled(arrivalEditable); - if (!arrivalEditable) { - clearArrivalFields(); - } + // Arrival delay is locked for starting wings (SP player wing or MP starting wing), since + // they must be present at mission start; the arrival location remains editable because it + // governs where subsequent waves arrive from + const bool arrivalDelayEditable = !isPlayerWing; + ui->arrivalDelaySpinBox->setEnabled(arrivalDelayEditable); + ui->minDelaySpinBox->setEnabled(arrivalDelayEditable); + ui->maxDelaySpinBox->setEnabled(arrivalDelayEditable); - // Arrival target/distance and path/custom buttons + // Arrival location, target/distance, and path/custom buttons const bool arrivalIsDockBay = _model->arrivalIsDockBay(); const bool arrivalNeedsTarget = _model->arrivalNeedsTarget(); - ui->arrivalTargetCombo->setEnabled(arrivalEditable && arrivalNeedsTarget); - ui->arrivalDistanceSpinBox->setEnabled(arrivalEditable && _model->arrivalNeedsDistance()); - ui->restrictArrivalPathsButton->setEnabled(arrivalEditable && arrivalIsDockBay); - ui->customWarpinButton->setEnabled(arrivalEditable && !arrivalIsDockBay); + ui->arrivalLocationCombo->setEnabled(true); + ui->arrivalTargetCombo->setEnabled(arrivalNeedsTarget); + ui->arrivalDistanceSpinBox->setEnabled(_model->arrivalNeedsDistance()); + ui->restrictArrivalPathsButton->setEnabled(arrivalIsDockBay); + ui->customWarpinButton->setEnabled(!arrivalIsDockBay); - // Arrival cue tree: lock when the wing actually contains Player-1 start (retail behavior) - ui->arrivalTree->setEnabled(!containsPlayerStart); + // Arrival cue tree: the wing containing the Player-1 start must be present at mission + // start, so its cue is only editable if it has multiple waves (where the cue governs + // the arrival of subsequent waves) + ui->arrivalTree->setEnabled(!containsPlayerStart || _model->getNumberOfWaves() > 1); - // Also tie the "no arrival warp" checkboxes to whether arrival is editable - ui->noArrivalWarpCheckBox->setEnabled(arrivalEditable); - ui->noArrivalWarpAdjustCheckbox->setEnabled(arrivalEditable); + // The "no arrival warp" checkboxes are always editable + ui->noArrivalWarpCheckBox->setEnabled(true); + ui->noArrivalWarpAdjustCheckbox->setEnabled(true); // Departure side: never gated by starting-wing rule ui->departureLocationCombo->setEnabled(true); diff --git a/qtfred/src/ui/util/ErrorChecker.cpp b/qtfred/src/ui/util/ErrorChecker.cpp index e1ab1e68bd0..16890260d6c 100644 --- a/qtfred/src/ui/util/ErrorChecker.cpp +++ b/qtfred/src/ui/util/ErrorChecker.cpp @@ -839,6 +839,26 @@ int ErrorChecker::checkPlayerWings() { Starting_wing_names[0], TVT_wing_names[0]); } + // The wing containing the player start must be present at mission start, so a custom arrival cue + // is only useful for controlling the arrival of subsequent waves. Flag a single-wave wing with a + // custom cue as a potential issue, but never change the cue itself. + for (auto &w : Wings) { + if (!w.wave_count || w.num_waves > 1) + continue; + if (w.arrival_cue == Locked_sexp_true) + continue; + + bool contains_player_start = false; + for (int j = 0; j < w.wave_count; j++) + if (Objects[Ships[w.ship_index[j]].objnum].type == OBJ_START) + contains_player_start = true; + + if (contains_player_start) { + potential("Wing \"%s\" contains a player start and a custom arrival cue, but only one wave. The arrival cue must evaluate to true at mission start, or the player will not be created.", + w.name); + } + } + auto checkMixedSpecies = [this](int w) { int species = -1; bool mixed = false;