From 53435532b055e65975389ee79d61026842b95bb1 Mon Sep 17 00:00:00 2001
From: Mike Nelson
Date: Wed, 6 May 2026 13:08:41 -0500
Subject: [PATCH 1/2] large ship collision group mission flag
---
code/mission/mission_flags.h | 1 +
code/mission/missionparse.cpp | 27 ++++++++++++++-
code/mission/missionparse.h | 1 +
code/missioneditor/missionsave.cpp | 4 +++
.../dialogs/MissionSpecDialogModel.cpp | 18 ++++++++++
.../mission/dialogs/MissionSpecDialogModel.h | 3 ++
qtfred/src/ui/dialogs/MissionSpecDialog.cpp | 15 +++++++++
qtfred/src/ui/dialogs/MissionSpecDialog.h | 2 ++
qtfred/ui/MissionSpecDialog.ui | 33 +++++++++++++++++++
9 files changed, 103 insertions(+), 1 deletion(-)
diff --git a/code/mission/mission_flags.h b/code/mission/mission_flags.h
index 178b1eddbfd..8189433525c 100644
--- a/code/mission/mission_flags.h
+++ b/code/mission/mission_flags.h
@@ -36,6 +36,7 @@ namespace Mission {
Neb2_fog_color_override, // Whether to use explicit fog colors instead of checking the palette - Goober5000
Fullneb_background_bitmaps, // Show background bitmaps despite fullneb
Preload_subspace, // Preload the subspace tunnel for both the sexp and specs checkbox (for scripts) - MjnMixael
+ Large_ships_no_collide_by_default, // Automatically puts all large ships in a shared collision group
NUM_VALUES
};
diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp
index 057a62afe4f..c575612facd 100644
--- a/code/mission/missionparse.cpp
+++ b/code/mission/missionparse.cpp
@@ -182,6 +182,8 @@ SCP_unordered_set Fred_migrated_immobile_ships;
int Num_path_restrictions;
path_restriction_t Path_restrictions[MAX_PATH_RESTRICTIONS];
+constexpr int DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP = 0;
+
extern int debrief_find_persona_index();
static bool mission_has_layer_name(const mission* pm, const SCP_string& layerName) {
@@ -397,7 +399,8 @@ flag_def_list_new Parse_mission_flags[] = {
{"Toggle Starting in Chase View", Mission::Mission_Flags::Toggle_start_chase_view, true, false},
{"Nebula Fog Color Override", Mission::Mission_Flags::Neb2_fog_color_override, true, true},
{"Full Nebula Background Bitmaps", Mission::Mission_Flags::Fullneb_background_bitmaps, true, true},
- {"Preload Subspace Tunnel", Mission::Mission_Flags::Preload_subspace, true, false}
+ {"Preload Subspace Tunnel", Mission::Mission_Flags::Preload_subspace, true, false},
+ {"Large Ships Do Not Collide By Default", Mission::Mission_Flags::Large_ships_no_collide_by_default, true, false}
};
parse_object_flag_description Parse_mission_flag_descriptions[] = {
@@ -431,6 +434,7 @@ parse_object_flag_description Parse_mission_flag_descrip
{Mission::Mission_Flags::Neb2_fog_color_override, "Whether to use explicit fog colors instead of checking the palette"},
{Mission::Mission_Flags::Fullneb_background_bitmaps, "Show background bitmaps despite full nebula"},
{Mission::Mission_Flags::Preload_subspace, "Preload the subspace tunnel for both the sexp and specs checkbox"},
+ {Mission::Mission_Flags::Large_ships_no_collide_by_default, "Automatically places all large ships in the configured collision group, preventing large ships from colliding with each other"},
};
const size_t Num_parse_mission_flags = sizeof(Parse_mission_flags) / sizeof(flag_def_list_new);
@@ -844,6 +848,18 @@ void parse_mission_info(mission *pm, bool basic = false)
stuff_int(&pm->contrail_threshold);
}
+ if (optional_string("+Large Ship Collision Group:")) {
+ stuff_int(&pm->large_ship_no_collide_collision_group);
+
+ if (pm->large_ship_no_collide_collision_group < 0 || pm->large_ship_no_collide_collision_group > 31) {
+ WarningEx(LOCATION,
+ "Invalid large ship collision group id %d specified. Valid IDs range from 0 to 31. Using group %d instead.\n",
+ pm->large_ship_no_collide_collision_group,
+ DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP);
+ pm->large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP;
+ }
+ }
+
if (optional_string("+Volumetric Nebula:")) {
pm->volumetrics.emplace().parse_volumetric_nebula();
}
@@ -2238,6 +2254,11 @@ int parse_create_object_sub(p_object *p_objp, bool standalone_ship)
// Goober5000 - set the collision group if one was provided
Objects[objnum].collision_group_id = p_objp->collision_group_id;
+ // Mission-level performance helper: large ships may be grouped so they skip mutual collision checks.
+ if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default] && sip->is_big_or_huge()) {
+ Objects[objnum].collision_group_id |= (1 << The_mission.large_ship_no_collide_collision_group);
+ }
+
// Goober5000 - set some fields that the mission log might need (if logged via parse_bring_in_docked_wing just below)
shipp->display_name = p_objp->display_name;
shipp->alt_type_index = p_objp->alt_type_index;
@@ -7178,6 +7199,7 @@ void mission::Reset()
envmap_name[ 0 ] = '\0';
contrail_threshold = CONTRAIL_THRESHOLD_DEFAULT;
+ large_ship_no_collide_collision_group = DEFAULT_LARGE_SHIP_NO_COLLIDE_COLLISION_GROUP;
ambient_light_level = DEFAULT_AMBIENT_LIGHT_LEVEL;
sound_environment.id = -1;
@@ -9557,6 +9579,9 @@ bool check_for_24_3_data()
bool check_for_25_1_data()
{
+ if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default])
+ return true;
+
if (count_items_with_value(Props) > 0)
return true;
diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h
index eea950e897e..3af9f4eb4e4 100644
--- a/code/mission/missionparse.h
+++ b/code/mission/missionparse.h
@@ -207,6 +207,7 @@ typedef struct mission {
char envmap_name[MAX_FILENAME_LEN];
int skybox_flags;
int contrail_threshold;
+ int large_ship_no_collide_collision_group;
int ambient_light_level;
std::optional volumetrics;
sound_env sound_environment;
diff --git a/code/missioneditor/missionsave.cpp b/code/missioneditor/missionsave.cpp
index e46fb6b590c..fb13384156b 100644
--- a/code/missioneditor/missionsave.cpp
+++ b/code/missioneditor/missionsave.cpp
@@ -2568,6 +2568,10 @@ int Fred_mission_save::save_mission_info()
if (The_mission.contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT) {
fout("\n$Contrail Speed Threshold: %d\n", The_mission.contrail_threshold);
}
+
+ if (The_mission.flags[Mission::Mission_Flags::Large_ships_no_collide_by_default]) {
+ fout("\n+Large Ship Collision Group: %d\n", The_mission.large_ship_no_collide_collision_group);
+ }
}
{
diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp
index fa8101a3de4..ffd6453dd9e 100644
--- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp
+++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp
@@ -70,6 +70,7 @@ void MissionSpecDialogModel::initializeData() {
_m_contrail_threshold = The_mission.contrail_threshold;
_m_contrail_threshold_flag = (_m_contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT);
+ _m_large_ship_no_collide_collision_group = The_mission.large_ship_no_collide_collision_group;
_m_custom_data = The_mission.custom_data;
_m_custom_strings = The_mission.custom_strings;
@@ -125,6 +126,7 @@ bool MissionSpecDialogModel::apply() {
The_mission.support_ships.max_support_ships = (_m_disallow_support) ? 0 : -1;
The_mission.support_ships.max_hull_repair_val = _m_max_hull_repair_val;
The_mission.support_ships.max_subsys_repair_val = _m_max_subsys_repair_val;
+ The_mission.large_ship_no_collide_collision_group = _m_large_ship_no_collide_collision_group;
// Copy mission flags
The_mission.flags = _m_flags;
@@ -418,6 +420,22 @@ void MissionSpecDialogModel::setMissionFlagDirect(Mission::Mission_Flags flag, b
}
}
+void MissionSpecDialogModel::setLargeShipNoCollideCollisionGroup(int group)
+{
+ if (group < 0) {
+ group = 0;
+ } else if (group > 31) {
+ group = 31;
+ }
+
+ modify(_m_large_ship_no_collide_collision_group, group);
+}
+
+int MissionSpecDialogModel::getLargeShipNoCollideCollisionGroup() const
+{
+ return _m_large_ship_no_collide_collision_group;
+}
+
bool MissionSpecDialogModel::getMissionFlag(Mission::Mission_Flags flag) const {
return _m_flags[flag];
}
diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h
index 0b623a3625a..636ac027b98 100644
--- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h
+++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h
@@ -42,6 +42,7 @@ class MissionSpecDialogModel : public AbstractDialogModel {
float _m_max_subsys_repair_val;
bool _m_contrail_threshold_flag;
int _m_contrail_threshold;
+ int _m_large_ship_no_collide_collision_group;
SCP_map _m_custom_data;
SCP_vector _m_custom_strings;
sound_env _m_sound_env;
@@ -119,6 +120,8 @@ class MissionSpecDialogModel : public AbstractDialogModel {
void setMissionFlag(const SCP_string& flag_name, bool enabled);
void setMissionFlagDirect(Mission::Mission_Flags flag, bool enabled);
+ void setLargeShipNoCollideCollisionGroup(int group);
+ int getLargeShipNoCollideCollisionGroup() const;
bool getMissionFlag(Mission::Mission_Flags flag) const;
const SCP_vector>& getMissionFlagsList();
static SCP_vector> getMissionFlagDescriptions();
diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp
index 339b9d60e19..a5ee42e72b8 100644
--- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp
+++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp
@@ -134,6 +134,7 @@ void MissionSpecDialog::initFlagList()
// per flag immediate apply to the model
connect(ui->flagList, &fso::fred::FlagListWidget::flagToggled, this, [this](const QString& name, bool checked) {
_model->setMissionFlag(name.toUtf8().constData(), checked);
+ updateLargeShipCollisionGroup();
});
}
@@ -149,6 +150,15 @@ void MissionSpecDialog::updateFlags()
}
ui->flagList->setFlags(toWidget);
+ updateLargeShipCollisionGroup();
+}
+
+void MissionSpecDialog::updateLargeShipCollisionGroup()
+{
+ const auto enabled = _model->getMissionFlag(Mission::Mission_Flags::Large_ships_no_collide_by_default);
+ ui->largeShipCollisionGroupLabel->setVisible(enabled);
+ ui->largeShipCollisionGroup->setVisible(enabled);
+ ui->largeShipCollisionGroup->setValue(_model->getLargeShipNoCollideCollisionGroup());
}
void MissionSpecDialog::updateMissionType() {
@@ -433,6 +443,11 @@ void MissionSpecDialog::on_musicPackCombo_currentIndexChanged(int index) {
_model->setSubEventMusic(subMusic);
}
+void MissionSpecDialog::on_largeShipCollisionGroup_valueChanged(int value)
+{
+ _model->setLargeShipNoCollideCollisionGroup(value);
+}
+
void MissionSpecDialog::on_aiProfileCombo_currentIndexChanged(int index)
{
auto aipIndex = ui->aiProfileCombo->itemData(index).value();
diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.h b/qtfred/src/ui/dialogs/MissionSpecDialog.h
index 64884aa02be..ed279f9acf0 100644
--- a/qtfred/src/ui/dialogs/MissionSpecDialog.h
+++ b/qtfred/src/ui/dialogs/MissionSpecDialog.h
@@ -64,6 +64,7 @@ private slots:
// Right column
// flags are dynamically generated and connected
+ void on_largeShipCollisionGroup_valueChanged(int value);
void on_aiProfileCombo_currentIndexChanged(int index);
// General
@@ -84,6 +85,7 @@ private slots:
void initFlagList();
void updateFlags();
+ void updateLargeShipCollisionGroup();
void updateMissionType();
void updateCmdMessage();
diff --git a/qtfred/ui/MissionSpecDialog.ui b/qtfred/ui/MissionSpecDialog.ui
index b3517fa6f48..ff6a6c9caa8 100644
--- a/qtfred/ui/MissionSpecDialog.ui
+++ b/qtfred/ui/MissionSpecDialog.ui
@@ -822,6 +822,39 @@
+ -
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Large Ship Collision Group
+
+
+
+ -
+
+
+ 0
+
+
+ 31
+
+
+
+
+
From 77ba0cd692be3e1226bc732656e613bd0eefb95c Mon Sep 17 00:00:00 2001
From: Mike Nelson
Date: Thu, 7 May 2026 16:16:43 -0500
Subject: [PATCH 2/2] update help doc
---
qtfred/help-src/doc/dialogs/MissionSpecsDialog.html | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html
index b05acd65eb1..399096ae618 100644
--- a/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html
+++ b/qtfred/help-src/doc/dialogs/MissionSpecsDialog.html
@@ -140,5 +140,15 @@ Flags
box to find a specific flag by name, or use Select All /
Select None to bulk-toggle. Hover over any flag label for a tooltip
description.
+Some flags reveal an additional control when enabled:
+
+ | Flag | Control | Description |
+ | Large Ships Do Not Collide By Default |
+ Large Ship Collision Group |
+ Collision group ID (0–31) that large and huge ships are automatically
+ placed into when the mission loads. Ships sharing the same group skip
+ collision checks against each other, reducing physics overhead in
+ missions with many capital ships. Default is 0. |
+