Skip to content

Commit 6c0d490

Browse files
authored
Qtfred error checker (scp-fs2open#7389)
* new qtfred error checker * fix some issues * clang * more clang * clean up and fixes * missed a comment * add help documentation * surface auto corrected issues more clearly * some cleanup
1 parent 1a34123 commit 6c0d490

29 files changed

Lines changed: 2675 additions & 1437 deletions

code/globalincs/pstypes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ const float PI_4 = (PI/4.0f);
364364

365365

366366
extern int Fred_running; // Is Fred running, or FreeSpace?
367+
extern int Qtfred_running; // Distinguishes QtFRED from legacy Fred2; Fred_running is set in both, but Qtfred_running only in QtFRED.
367368
extern bool running_unittests;
368369

369370
const size_t INVALID_SIZE = static_cast<size_t>(-1);

code/mission/missionparse.cpp

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,27 @@ p_object *Player_start_pobject;
175175
// something before that ship has even been loaded yet)
176176
SCP_vector<SCP_string> Parse_names;
177177

178+
SCP_vector<SCP_string> Mission_parse_warnings;
179+
180+
// Routes a parse-time auto-correction notice to the right surface for the app:
181+
// outside QtFRED, the existing Warning(LOCATION, ...) popup; inside QtFRED, the
182+
// Mission_parse_warnings queue so the ErrorChecker can present it without a popup.
183+
static void parse_warning_or_record(SCP_FORMAT_STRING const char* fmt, ...) SCP_FORMAT_STRING_ARGS(1, 2);
184+
static void parse_warning_or_record(const char* fmt, ...)
185+
{
186+
SCP_string msg;
187+
va_list args;
188+
va_start(args, fmt);
189+
vsprintf(msg, fmt, args);
190+
va_end(args);
191+
192+
if (Qtfred_running) {
193+
Mission_parse_warnings.push_back(std::move(msg));
194+
} else {
195+
Warning(LOCATION, "%s", msg.c_str());
196+
}
197+
}
198+
178199
SCP_vector<texture_replace> Fred_texture_replacements;
179200

180201
SCP_unordered_set<int> Fred_migrated_immobile_ships;
@@ -2426,10 +2447,10 @@ int parse_create_object_sub(p_object *p_objp, bool standalone_ship)
24262447
// will accept were apparently written out incorrectly with Fred. This Int3() should
24272448
// trap these instances.
24282449
#ifndef NDEBUG
2429-
if (Fred_running)
2450+
if (Fred_running && !Qtfred_running)
24302451
{
24312452
std::set<size_t> default_orders, remaining_orders;
2432-
2453+
24332454
default_orders = ship_get_default_orders_accepted(&Ship_info[shipp->ship_info_index]);
24342455
std::set_difference(p_objp->orders_accepted.begin(), p_objp->orders_accepted.end(), default_orders.begin(), default_orders.end(),
24352456
std::inserter(remaining_orders, remaining_orders.begin()));
@@ -2963,7 +2984,7 @@ void resolve_parse_flags(object *objp, flagset<Mission::Parse_Object_Flags> &par
29632984

29642985
if ((parse_flags[Mission::Parse_Object_Flags::OF_No_shields]) && (parse_flags[Mission::Parse_Object_Flags::OF_Force_shields_on]))
29652986
{
2966-
Warning(LOCATION, "The parser found a ship with both the \"force-shields-on\" and \"no-shields\" flags; this is inconsistent!");
2987+
parse_warning_or_record("Ship %s has both the \"force-shields-on\" and \"no-shields\" flags; this is inconsistent.", shipp->ship_name);
29672988
}
29682989
if (parse_flags[Mission::Parse_Object_Flags::OF_No_shields])
29692990
objp->flags.set(Object::Object_Flags::No_shields);
@@ -3458,7 +3479,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp)
34583479
|| (p_objp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (p_objp->arrival_location == ArrivalLocation::BELOW_SHIP)
34593480
|| (p_objp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (p_objp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) ))
34603481
{
3461-
Warning(LOCATION, "Arrival distance for ship %s cannot be %d. Setting to 1.\n", p_objp->name, p_objp->arrival_distance);
3482+
parse_warning_or_record("Arrival distance for ship %s cannot be %d — corrected to 1.", p_objp->name, p_objp->arrival_distance);
34623483
p_objp->arrival_distance = 1;
34633484
}
34643485
}
@@ -3483,7 +3504,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp)
34833504
stuff_int(&delay);
34843505
if (delay < 0)
34853506
{
3486-
Warning(LOCATION, "Cannot have arrival delay < 0 on ship %s", p_objp->name);
3507+
parse_warning_or_record("Arrival delay on ship %s cannot be negative — corrected to 0.", p_objp->name);
34873508
delay = 0;
34883509
}
34893510

@@ -3520,7 +3541,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp)
35203541
stuff_int(&delay);
35213542
if (delay < 0)
35223543
{
3523-
Warning(LOCATION, "Cannot have departure delay < 0 (ship %s)", p_objp->name);
3544+
parse_warning_or_record("Departure delay on ship %s cannot be negative — corrected to 0.", p_objp->name);
35243545
delay = 0;
35253546
}
35263547

@@ -3752,7 +3773,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp)
37523773
stuff_int(&p_objp->destroy_before_mission_time);
37533774
if (p_objp->destroy_before_mission_time < 0)
37543775
{
3755-
Warning(LOCATION, "Cannot set a negative 'destroy before mission' value (ship %s)", p_objp->name);
3776+
parse_warning_or_record("'Destroy before mission' value on ship %s cannot be negative — corrected to 0.", p_objp->name);
37563777
p_objp->destroy_before_mission_time = 0;
37573778
}
37583779

@@ -3847,7 +3868,7 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp)
38473868
if (optional_string("+Persona Index:")) {
38483869
stuff_int(&p_objp->persona_index);
38493870
if (p_objp->persona_index < -1 || p_objp->persona_index >= (int)Personas.size()) {
3850-
Warning(LOCATION, "Persona index %d for %s is out of range! Setting to -1.", p_objp->persona_index, p_objp->name);
3871+
parse_warning_or_record("Persona index %d for ship %s is out of range — corrected to -1.", p_objp->persona_index, p_objp->name);
38513872
p_objp->persona_index = -1;
38523873
}
38533874
}
@@ -4915,7 +4936,7 @@ void parse_wing(mission *pm)
49154936
|| (wingp->arrival_location == ArrivalLocation::ABOVE_SHIP) || (wingp->arrival_location == ArrivalLocation::BELOW_SHIP)
49164937
|| (wingp->arrival_location == ArrivalLocation::TO_LEFT_OF_SHIP) || (wingp->arrival_location == ArrivalLocation::TO_RIGHT_OF_SHIP) ))
49174938
{
4918-
Warning(LOCATION, "Arrival distance for wing %s cannot be %d. Setting to 1.\n", wingp->name, wingp->arrival_distance);
4939+
parse_warning_or_record("Arrival distance for wing %s cannot be %d — corrected to 1.", wingp->name, wingp->arrival_distance);
49194940
wingp->arrival_distance = 1;
49204941
}
49214942
}
@@ -4940,7 +4961,7 @@ void parse_wing(mission *pm)
49404961
stuff_int(&delay);
49414962
if (delay < 0)
49424963
{
4943-
Warning(LOCATION, "Cannot have arrival delay < 0 on wing %s", wingp->name);
4964+
parse_warning_or_record("Arrival delay on wing %s cannot be negative — corrected to 0.", wingp->name);
49444965
delay = 0;
49454966
}
49464967

@@ -4977,7 +4998,7 @@ void parse_wing(mission *pm)
49774998
stuff_int(&delay);
49784999
if (delay < 0)
49795000
{
4980-
Warning(LOCATION, "Cannot have departure delay < 0 on wing %s", wingp->name);
5001+
parse_warning_or_record("Departure delay on wing %s cannot be negative — corrected to 0.", wingp->name);
49815002
delay = 0;
49825003
}
49835004

@@ -5152,7 +5173,7 @@ void parse_wing(mission *pm)
51525173

51535174
// Goober5000 - if this is a player start object, there shouldn't be a wing arrival delay (Mantis #2678)
51545175
if ((p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]) && (wingp->arrival_delay != 0)) {
5155-
Warning(LOCATION, "Wing %s specifies an arrival delay of %ds, but it also contains a player. The arrival delay will be reset to 0.", wingp->name, abs(wingp->arrival_delay));
5176+
parse_warning_or_record("Wing %s specifies an arrival delay of %ds, but it also contains a player — corrected to 0.", wingp->name, abs(wingp->arrival_delay));
51565177
if (!Fred_running && wingp->arrival_delay > 0) {
51575178
// timestamp has been set, so set it again
51585179
wingp->arrival_delay = timestamp(0);
@@ -5423,7 +5444,9 @@ void post_process_ships_wings()
54235444
// error checking for custom wings
54245445
if (strcmp(Starting_wing_names[0], TVT_wing_names[0]) != 0)
54255446
{
5426-
Error(LOCATION, "The first starting wing and the first team-versus-team wing must have the same wing name.\n");
5447+
// In QtFRED this is surfaced via ErrorChecker::checkPlayerWings so the editor can load the mission.
5448+
if (!Qtfred_running)
5449+
Error(LOCATION, "The first starting wing and the first team-versus-team wing must have the same wing name.\n");
54275450
}
54285451

54295452
// set up wing indexes
@@ -5610,7 +5633,7 @@ void post_process_ships_wings()
56105633
for (int i = 1; i < MAX_STARTING_WINGS; i++) {
56115634
// If there was a wing for this squadron entry, check the last one. If it's empty, we found a mistake, so move the wing names over.
56125635
if (Squadron_wing_names_found[i] && !Squadron_wing_names_found[i - 1]) {
5613-
Warning(LOCATION, "Squadron wings are not in the correct order and may cause wings to disappear in multi.\n\nEither wing %s should exist or the %s entry needs to come before it in the list.\n\nPlease go back and fix the mission.", Squadron_wing_names[i - 1], Squadron_wing_names[i]);
5636+
parse_warning_or_record("Squadron wings are not in the correct order and may cause wings to disappear in multi. Either wing %s should exist or the %s entry needs to come before it in the list — wing names have been swapped.", Squadron_wing_names[i - 1], Squadron_wing_names[i]);
56145637
char temp_chars[NAME_LENGTH];
56155638
strcpy_s(temp_chars, Squadron_wing_names[i - 1]);
56165639
strcpy_s(Squadron_wing_names[i - 1], Squadron_wing_names[i]);
@@ -5648,7 +5671,7 @@ void parse_event(mission *pm)
56485671
// sanity check on the repeat count variable
56495672
// _argv[-1] - negative repeat count is now legal; means repeat indefinitely.
56505673
if ( event->repeat_count == 0 ){
5651-
Warning(LOCATION, "Repeat count for mission event %s is 0.\nMust be >= 1 or negative! Setting to 1.", event->name.c_str() );
5674+
parse_warning_or_record("Repeat count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str());
56525675
event->repeat_count = 1;
56535676
}
56545677
}
@@ -5665,7 +5688,7 @@ void parse_event(mission *pm)
56655688
// sanity check on the trigger count variable
56665689
// negative trigger count is also legal
56675690
if ( event->trigger_count == 0 ){
5668-
Warning(LOCATION, "Trigger count for mission event %s is 0.\nMust be >= 1 or negative! Setting to 1.", event->name.c_str() );
5691+
parse_warning_or_record("Trigger count for mission event %s is 0 — must be >= 1 or negative; corrected to 1.", event->name.c_str());
56695692
event->trigger_count = 1;
56705693
}
56715694
}
@@ -6021,7 +6044,7 @@ void parse_reinforcement(mission *pm)
60216044
stuff_int(&delay);
60226045
if (delay < 0)
60236046
{
6024-
Warning(LOCATION, "Cannot have arrival delay < 0 on reinforcement %s", ptr->name);
6047+
parse_warning_or_record("Arrival delay on reinforcement %s cannot be negative — corrected to 0.", ptr->name);
60256048
delay = 0;
60266049
}
60276050

@@ -6041,14 +6064,14 @@ void parse_reinforcement(mission *pm)
60416064

60426065
if (rforce_obj == NULL) {
60436066
if ((instance = wing_name_lookup(ptr->name, 1)) == -1) {
6044-
Warning(LOCATION, "Reinforcement %s not found as ship or wing", ptr->name);
6067+
parse_warning_or_record("Reinforcement %s not found as ship or wing — declaration ignored.", ptr->name);
60456068
return;
60466069
}
60476070
} else {
60486071
// Individual ships in wings can't be reinforcements - FUBAR
60496072
if (rforce_obj->wingnum >= 0)
60506073
{
6051-
Warning(LOCATION, "Reinforcement %s is part of a wing - Ignoring reinforcement declaration", ptr->name);
6074+
parse_warning_or_record("Reinforcement %s is part of a wing reinforcement declaration ignored.", ptr->name);
60526075
return;
60536076
}
60546077
else
@@ -6698,6 +6721,9 @@ bool parse_mission(mission *pm, int flags)
66986721
int saved_warning_count = Global_warning_count;
66996722
int saved_error_count = Global_error_count;
67006723

6724+
// Reset the parse-time warning queue so each load starts fresh (only consumed by QtFRED).
6725+
Mission_parse_warnings.clear();
6726+
67016727
// reset parse error stuff
67026728
Num_unknown_ship_classes = 0;
67036729
Num_unknown_prop_classes = 0;
@@ -6798,7 +6824,9 @@ bool parse_mission(mission *pm, int flags)
67986824
if (!post_process_mission(pm))
67996825
return false;
68006826

6801-
if ((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0) {
6827+
// QtFRED surfaces parse issues through its own error checker, so skip this summary popup there; Fred2 and the game still show it.
6828+
if (!Qtfred_running &&
6829+
((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0)) {
68026830
char text[512];
68036831
sprintf(text, "Warning!\n\nThe current mission has generated %d warnings and/or errors during load. These are usually caused by corrupted ship models or syntax errors in the mission file. While FreeSpace Open will attempt to compensate for these issues, it cannot guarantee a trouble-free gameplay experience. Source Code Project staff cannot provide assistance or support for these problems, as they are caused by the mission's data files, not FreeSpace Open's source code.", (saved_warning_count - Global_warning_count) + (saved_error_count - Global_error_count));
68046832
popup(PF_TITLE_BIG | PF_TITLE_RED | PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, text);
@@ -6936,7 +6964,9 @@ bool post_process_mission(mission *pm)
69366964
error_msg += "\n\n(Bad node appears to be: ";
69376965
error_msg += bad_node_str;
69386966
error_msg += ")\n";
6939-
Warning(LOCATION, "%s", error_msg.c_str());
6967+
// QtFRED surfaces SEXP errors through ErrorChecker's fred_check_sexp; skip the popup there.
6968+
if (!Qtfred_running)
6969+
Warning(LOCATION, "%s", error_msg.c_str());
69406970

69416971
// syntax errors are recoverable in Fred but not FS
69426972
if (!Fred_running && !sexp_recoverable_error(result)) {
@@ -7734,15 +7764,17 @@ void mission_parse_set_up_initial_docks()
77347764
// display an error if necessary
77357765
if (dfi.maintained_variables.int_value == 0)
77367766
{
7737-
Warning(LOCATION, "In the docking group containing %s, every ship has an arrival cue set to false. The group will not appear in-mission!\n", pobjp->name);
7767+
if (!Qtfred_running)
7768+
Warning(LOCATION, "In the docking group containing %s, every ship has an arrival cue set to false. The group will not appear in-mission!\n", pobjp->name);
77387769

77397770
// for FRED, we must arbitrarily choose a dock leader, otherwise the entire docked group will not be loaded
77407771
if (Fred_running)
77417772
dock_evaluate_all_docked_objects(pobjp, &dfi, parse_object_choose_arbitrary_dock_leader_helper);
77427773
}
77437774
else if (dfi.maintained_variables.int_value > 1)
77447775
{
7745-
Warning(LOCATION, "In the docking group containing %s, there is more than one ship with a non-false arrival cue! There can only be one such ship. Setting all arrival cues except %s to false...\n", dfi.maintained_variables.objp_value->name, dfi.maintained_variables.objp_value->name);
7776+
if (!Qtfred_running)
7777+
Warning(LOCATION, "In the docking group containing %s, there is more than one ship with a non-false arrival cue! There can only be one such ship. Setting all arrival cues except %s to false...\n", dfi.maintained_variables.objp_value->name, dfi.maintained_variables.objp_value->name);
77467778
}
77477779

77487780
// clear dfi stuff

code/mission/missionparse.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,11 @@ extern fix Mission_end_time;
566566

567567
extern SCP_vector<SCP_string> Parse_names;
568568

569+
// Populated when Qtfred_running and a parse-time auto-correction fires. Drained by
570+
// QtFRED's ErrorChecker so the corrections are visible to the designer instead of
571+
// silently buried. Outside of QtFRED these sites still call Warning(LOCATION, ...).
572+
extern SCP_vector<SCP_string> Mission_parse_warnings;
573+
569574
extern char Player_start_shipname[NAME_LENGTH];
570575
extern int Player_start_shipnum;
571576
extern p_object *Player_start_pobject;

fred2/fred.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pending_message Pending_messages[MAX_PENDING_MESSAGES];
106106
CFREDApp theApp;
107107

108108
int Fred_running = 1;
109+
int Qtfred_running = 0;
109110
int FrameCount = 0;
110111
bool Fred_active = true;
111112
int Update_window = 1;

freespace2/freespace.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ int Show_net_stats;
399399
bool Pre_player_entry = false;
400400

401401
int Fred_running = 0;
402+
int Qtfred_running = 0;
402403
bool running_unittests = false;
403404

404405
// required for hudtarget... kinda dumb, but meh

qtfred/help-src/doc/dialogs/ErrorCheckerDialog.html

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,82 @@
88
<body>
99
<nav class="breadcrumb"><a href="../index.html">QtFRED Help</a> &rsaquo; Tools &rsaquo; Error Checker</nav>
1010
<h1>Error Checker</h1>
11-
<p>Accessible via <strong>Tools &rsaquo; Error Checker</strong>.</p>
12-
<div class="stub-notice">Full documentation for this tool is not yet written.</div>
11+
<p>Opens via <strong>Tools &rsaquo; Error Checker</strong>. Also runs automatically
12+
before saving when issues are present.</p>
13+
<p>Scans the entire mission for structural problems, design errors, configuration
14+
warnings, and potential issues. Results are listed by severity with a description
15+
of each problem found.</p>
16+
17+
<h2>Controls</h2>
18+
<table>
19+
<tr><th>Control</th><th>Description</th></tr>
20+
<tr><td>Rerun</td><td>Runs the check again. Use this after making fixes to confirm
21+
they are resolved.</td></tr>
22+
<tr><td>Check Potential Issues</td><td>When checked, Potential Issue entries are
23+
included in the results alongside errors and warnings. When unchecked,
24+
potential issues are silently omitted.</td></tr>
25+
<tr><td>Apply Auto-Corrections</td><td>When checked, FRED automatically corrects
26+
issues it can resolve without user input each time a check runs. Only applies
27+
to correctable errors and warnings; Critical Errors are never auto-corrected.
28+
Auto-corrections are applied before results are displayed.</td></tr>
29+
<tr><td>Status bar</td><td>Shows a summary of the most recent check: the total
30+
number of entries found, or <em>No check has been run yet</em> before the
31+
first run.</td></tr>
32+
</table>
33+
34+
<h2>Severity levels</h2>
35+
<table>
36+
<tr><th>Severity</th><th>Meaning</th></tr>
37+
<tr><td>Critical Error</td><td>A serious structural problem that FRED cannot
38+
automatically correct. The mission may fail to load or behave unpredictably
39+
in-game. Must be fixed manually before the mission is usable.</td></tr>
40+
<tr><td>Error</td><td>A mission design problem that must be fixed. The mission may
41+
not play correctly.</td></tr>
42+
<tr><td>Warning</td><td>A problem that can be (or has been) auto-corrected. Review
43+
the change before saving.</td></tr>
44+
<tr><td>Potential Issue</td><td>A situation that may be intentional but is worth
45+
reviewing. Only shown when <strong>Check Potential Issues</strong> is
46+
checked.</td></tr>
47+
</table>
48+
49+
<h2>What is checked</h2>
50+
<p>The checker inspects the following areas of the mission:</p>
51+
<ul>
52+
<li>Object list integrity and name uniqueness</li>
53+
<li>Ships — SEXPs, AI goals, anchor references, docking configuration, and loadout
54+
weapon availability</li>
55+
<li>Wings — structure, SEXPs, AI goals, anchor references, wave thresholds, and
56+
accepted orders consistency</li>
57+
<li>Player starts and player wing membership</li>
58+
<li>Waypoint path names for conflicts with object names</li>
59+
<li>Reinforcement name references</li>
60+
<li>Mission events and mission objectives — SEXP validation</li>
61+
<li>Briefings — icon ID uniqueness</li>
62+
<li>Debriefings — SEXP validation</li>
63+
<li>Asteroid field target ship name validity</li>
64+
<li>Initially-docked groups — must have exactly one non-false arrival cue</li>
65+
<li>Team loadout — weapons used in starting wings must be present in the
66+
team loadout pool</li>
67+
</ul>
68+
69+
<h2>Pre-save mode</h2>
70+
<p>When saving a mission that has unresolved errors, the Error Checker opens
71+
automatically in pre-save mode. In this mode three additional buttons appear at the
72+
bottom of the dialog:</p>
73+
<table>
74+
<tr><th>Button</th><th>Description</th></tr>
75+
<tr><td>Cancel</td><td>Aborts the save and returns to the editor so issues can be
76+
addressed.</td></tr>
77+
<tr><td>Save As Is</td><td>Saves the mission despite the reported issues.</td></tr>
78+
<tr><td>Fix and Save</td><td>Applies all available auto-corrections and then saves.
79+
Only available when <strong>Apply Auto-Corrections</strong> is
80+
checked.</td></tr>
81+
</table>
82+
83+
<h2>Preferences</h2>
84+
<p>The <strong>Check Potential Issues</strong> and <strong>Apply Auto-Corrections</strong>
85+
settings are also found in <strong>File &rsaquo; Preferences &rsaquo; Error
86+
Checker</strong>. They share the same stored value — changing one updates the other.
87+
Use Preferences to set the default behavior for all future check runs.</p>
1388
</body>
1489
</html>

0 commit comments

Comments
 (0)