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;