Skip to content

Commit 170a4a8

Browse files
authored
Add mission templates to QtFRED (#7320)
* add mission templates * fix for unique_ptr
1 parent 409421a commit 170a4a8

14 files changed

Lines changed: 796 additions & 4 deletions

code/mission/missionparse.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ extern bool check_for_25_1_data();
6666
#define MPF_ONLY_MISSION_INFO (1 << 0)
6767
#define MPF_IMPORT_FSM (1 << 1)
6868
#define MPF_FAST_RELOAD (1 << 2) // skip clearing some stuff so we can load the mission faster (usually since it's the same mission)
69+
#define MPF_IS_TEMPLATE (1 << 3) // loading a .fst mission template; post-load reset of name, author, timestamps, notes, description, and camera
6970

7071
// bitfield definitions for missions game types
7172
#define OLD_MAX_GAME_TYPES 4 // needed for compatibility

code/missioneditor/missionsave.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2441,6 +2441,43 @@ int Fred_mission_save::save_mission_file(const char* pathname)
24412441
return err;
24422442
}
24432443

2444+
int Fred_mission_save::save_template_file(const char* pathname)
2445+
{
2446+
char savepath[MAX_PATH_LEN];
2447+
2448+
strcpy_s(savepath, "");
2449+
auto p = strrchr(pathname, DIR_SEPARATOR_CHAR);
2450+
if (p) {
2451+
auto len = p - pathname;
2452+
strncpy(savepath, pathname, len);
2453+
savepath[len] = '\0';
2454+
strcat_s(savepath, DIR_SEPARATOR_STR);
2455+
}
2456+
strcat_s(savepath, "saving.xxx");
2457+
2458+
save_mission_internal(savepath);
2459+
2460+
if (!err) {
2461+
// Templates don't get .bak backups; just overwrite directly
2462+
cf_delete(pathname, CF_TYPE_MISSIONS);
2463+
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
2464+
}
2465+
2466+
return err;
2467+
}
2468+
2469+
void Fred_mission_save::save_template_info()
2470+
{
2471+
const auto& ti = save_config.template_info;
2472+
2473+
fout("#Template Info\n");
2474+
fout("\n$Template Title: %s", ti.title.c_str());
2475+
fout("\n$Template Author: %s", ti.author.c_str());
2476+
fout("\n$Template Tags: %s", ti.tags.c_str());
2477+
fout("\n$Template Description:\n%s\n$end_template_desc", ti.description.c_str());
2478+
fout("\n\n#End Template Info\n\n");
2479+
}
2480+
24442481
int Fred_mission_save::save_mission_info()
24452482
{
24462483
required_string_fred("#Mission Info");
@@ -3065,6 +3102,9 @@ void Fred_mission_save::save_mission_internal(const char* pathname)
30653102
return;
30663103
}
30673104

3105+
if (!save_config.template_info.title.empty())
3106+
save_template_info();
3107+
30683108
// Goober5000
30693109
convert_special_tags_to_retail();
30703110

code/missioneditor/missionsave.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ enum class MissionFormat {
1919
COMPATIBILITY_MODE = 2
2020
};
2121

22+
struct MissionTemplateInfo {
23+
SCP_string title;
24+
SCP_string author;
25+
SCP_string tags;
26+
SCP_string description;
27+
};
28+
2229
struct FredSaveConfig {
2330

2431
vec3d view_pos{};
@@ -36,6 +43,8 @@ struct FredSaveConfig {
3643

3744
int mission_backup_depth = MISSION_BACKUP_DEPTH; // TODO make user configurable
3845
SCP_string mission_backup_name = MISSION_BACKUP_NAME; // TODO make user configurable
46+
47+
MissionTemplateInfo template_info;
3948
};
4049

4150
/**
@@ -51,6 +60,7 @@ class Fred_mission_save {
5160
Fred_mission_save() = default;
5261

5362
void set_save_format(MissionFormat fmt) { save_config.save_format = fmt; }
63+
void set_template_info(const MissionTemplateInfo& info) { save_config.template_info = info; }
5464
void set_view_pos(const vec3d& pos) { save_config.view_pos = pos; }
5565
void set_view_orient(const matrix& orient) { save_config.view_orient = orient; }
5666
void set_fred_alt_names(const char (*names)[NAME_LENGTH + 1]) { save_config.fred_alt_names = names; }
@@ -88,6 +98,21 @@ class Fred_mission_save {
8898
*/
8999
int save_mission_file(const char* pathname);
90100

101+
/**
102+
* @brief Saves a mission template (.fst) to the given full pathname
103+
*
104+
* @param[in] pathname The full pathname to save to
105+
*
106+
* @details Saves a complete mission file and prepends a #Template Info section
107+
* containing display metadata (title, author, tags, description) from
108+
* save_config.template_info. The metadata is used by the template browser
109+
* and is ignored by the normal mission parser.
110+
*
111+
* @returns 0 for no error, or
112+
* @returns A negative value if an error occurred
113+
*/
114+
int save_template_file(const char* pathname);
115+
91116
protected:
92117

93118
FredSaveConfig save_config{};
@@ -168,6 +193,11 @@ class Fred_mission_save {
168193
*/
169194
void save_ai_goals(ai_goal* goalp, int ship);
170195

196+
/**
197+
* @brief Writes the #Template Info section to the top of the file
198+
*/
199+
void save_template_info();
200+
171201
/**
172202
* @brief Saves the skybox bitmaps
173203
*

qtfred/source_groups.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,16 @@ add_file_folder("Source/UI/Dialogs"
188188
src/ui/dialogs/ReinforcementsEditorDialog.h
189189
src/ui/dialogs/RelativeCoordinatesDialog.cpp
190190
src/ui/dialogs/RelativeCoordinatesDialog.h
191+
src/ui/dialogs/SaveAsTemplateDialog.cpp
192+
src/ui/dialogs/SaveAsTemplateDialog.h
191193
src/ui/dialogs/SelectionDialog.cpp
192194
src/ui/dialogs/SelectionDialog.h
193195
src/ui/dialogs/ShieldSystemDialog.h
194196
src/ui/dialogs/ShieldSystemDialog.cpp
195197
src/ui/dialogs/TeamLoadoutDialog.cpp
196198
src/ui/dialogs/TeamLoadoutDialog.h
199+
src/ui/dialogs/TemplateBrowserDialog.cpp
200+
src/ui/dialogs/TemplateBrowserDialog.h
197201
src/ui/dialogs/VariableDialog.cpp
198202
src/ui/dialogs/VariableDialog.h
199203
src/ui/dialogs/VoiceActingManager.h
@@ -338,6 +342,8 @@ add_file_folder("UI"
338342
ui/ShipWeaponsDialog.ui
339343
ui/VariableDialog.ui
340344
ui/WingEditorDialog.ui
345+
ui/SaveAsTemplateDialog.ui
346+
ui/TemplateBrowserDialog.ui
341347
)
342348

343349
add_file_folder("Resources"

qtfred/src/mission/Editor.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,27 @@ bool Editor::loadMission(const std::string& mission_name, int flags) {
400400
viewport->view_orient = Parse_viewer_orient;
401401
viewport->view_pos = Parse_viewer_pos;
402402
}
403+
404+
if (flags & MPF_IS_TEMPLATE) {
405+
// reset fields that should not carry over from the template source
406+
strcpy_s(The_mission.name, "Untitled");
407+
The_mission.author = getUsername();
408+
409+
time_t currentTime;
410+
time(&currentTime);
411+
auto timeinfo = localtime(&currentTime);
412+
time_to_mission_info_string(timeinfo, The_mission.created, DATE_TIME_LENGTH - 1);
413+
strcpy_s(The_mission.modified, The_mission.created);
414+
415+
strcpy_s(The_mission.notes, "This is a FRED2_OPEN created mission.");
416+
strcpy_s(The_mission.mission_desc, "Put mission description here");
417+
418+
for (auto& viewport : _viewports) {
419+
viewport->resetView();
420+
viewport->resetViewPhysics();
421+
}
422+
}
423+
403424
stars_post_level_init();
404425

405426
missionLoaded(filepath);

qtfred/src/ui/FredView.cpp

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "FredView.h"
22
#include "ui_FredView.h"
33

4+
#include <QDir>
45
#include <QFileDialog>
56
#include <QMessageBox>
67
#include <QDebug>
@@ -43,11 +44,14 @@
4344
#include <ui/dialogs/MusicPlayerDialog.h>
4445
#include <ui/dialogs/RelativeCoordinatesDialog.h>
4546
#include <ui/dialogs/ControlsDialog.h>
47+
#include <ui/dialogs/SaveAsTemplateDialog.h>
48+
#include <ui/dialogs/TemplateBrowserDialog.h>
4649
#include <ui/ControlBindings.h>
4750
#include <iff_defs/iff_defs.h>
4851

4952
#include "mission/Editor.h"
5053
#include "mission/management.h"
54+
#include "mission/missionparse.h"
5155
#include "missioneditor/missionsave.h"
5256

5357
#include "widgets/ColorComboBox.h"
@@ -171,7 +175,7 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) {
171175
connect(this, &FredView::viewIdle, this, [this]() { ui->actionError_Checker_Checks_Potential_Issues->setChecked(_viewport->Error_checker_checks_potential_issues); });
172176
}
173177

174-
void FredView::loadMissionFile(const QString& pathName) {
178+
void FredView::loadMissionFile(const QString& pathName, int flags) {
175179
if (!maybePromptToSaveMissionChanges(tr("loading another mission"))) {
176180
return;
177181
}
@@ -184,9 +188,10 @@ void FredView::loadMissionFile(const QString& pathName) {
184188
fred->clean_up_selections();
185189

186190
auto pathToLoad = pathName.toStdString();
187-
fred->maybeUseAutosave(pathToLoad);
191+
if (!(flags & MPF_IS_TEMPLATE))
192+
fred->maybeUseAutosave(pathToLoad);
188193

189-
fred->loadMission(pathToLoad);
194+
fred->loadMission(pathToLoad, flags);
190195

191196
QApplication::restoreOverrideCursor();
192197
} catch (const fso::fred::mission_load_error&) {
@@ -273,6 +278,68 @@ bool FredView::saveMissionAs() {
273278
return true;
274279
}
275280

281+
void FredView::saveAsTemplate() {
282+
// Collect template metadata first
283+
dialogs::SaveAsTemplateDialog metaDialog(this, getUsername());
284+
if (metaDialog.exec() != QDialog::Accepted)
285+
return;
286+
287+
// Default to data/missions/templates/ and create it if needed
288+
QString templatesDir = QDir::currentPath() + "/data/missions/templates";
289+
QDir().mkpath(templatesDir);
290+
291+
QString templateName = QFileDialog::getSaveFileName(this,
292+
tr("Save As Template"),
293+
templatesDir,
294+
tr("FS2 mission templates (*.fst)"));
295+
296+
if (templateName.isEmpty())
297+
return;
298+
299+
if (!templateName.endsWith(".fst", Qt::CaseInsensitive))
300+
templateName += ".fst";
301+
302+
Fred_mission_save save;
303+
save.set_always_save_display_names(_viewport->Always_save_display_names);
304+
save.set_fred_alt_names(Fred_alt_names);
305+
save.set_fred_callsigns(Fred_callsigns);
306+
save.set_template_info(metaDialog.templateInfo());
307+
308+
save.save_template_file(templateName.replace('/', DIR_SEPARATOR_CHAR).toUtf8().constData());
309+
}
310+
311+
void FredView::loadTemplate() {
312+
QString templatesDir = QDir::currentPath() + "/data/missions/templates";
313+
QDir().mkpath(templatesDir);
314+
315+
dialogs::TemplateBrowserDialog browser(this, templatesDir);
316+
if (browser.exec() != QDialog::Accepted)
317+
return;
318+
319+
QString templateName = browser.selectedTemplatePath();
320+
if (templateName.isEmpty())
321+
return;
322+
323+
auto result = QMessageBox::question(this,
324+
tr("Load Template"),
325+
tr("This will replace all mission data. Continue?"),
326+
QMessageBox::Yes | QMessageBox::No,
327+
QMessageBox::No);
328+
329+
if (result != QMessageBox::Yes)
330+
return;
331+
332+
loadMissionFile(templateName.replace('/', DIR_SEPARATOR_CHAR), MPF_IS_TEMPLATE);
333+
}
334+
335+
void FredView::on_actionLoad_Template_triggered(bool) {
336+
loadTemplate();
337+
}
338+
339+
void FredView::on_actionSave_As_Template_triggered(bool) {
340+
saveAsTemplate();
341+
}
342+
276343
void FredView::on_mission_loaded(const std::string& filepath) {
277344
QString filename = "Untitled";
278345
if (!filepath.empty()) {
@@ -304,6 +371,10 @@ void FredView::newMission() {
304371
fred->createNewMission();
305372
}
306373
void FredView::addToRecentFiles(const QString& path) {
374+
// Templates are not mission files; don't pollute the recent list with them
375+
if (path.endsWith(".fst", Qt::CaseInsensitive))
376+
return;
377+
307378
// First get the list of existing files
308379
QSettings settings;
309380
auto recentFiles = settings.value("FredView/recentFiles").toStringList();

qtfred/src/ui/FredView.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FredView: public QMainWindow, public IDialogProvider {
3939
~FredView() override;
4040
void setEditor(Editor* editor, EditorViewport* viewport);
4141

42-
void loadMissionFile(const QString& pathName);
42+
void loadMissionFile(const QString& pathName, int flags = 0);
4343

4444
QSurface* getRenderSurface();
4545
RenderWidget* getRenderWidget();
@@ -58,6 +58,8 @@ class FredView: public QMainWindow, public IDialogProvider {
5858
void on_actionSave_As_triggered(bool);
5959
void on_actionSave_triggered(bool);
6060
void on_actionExit_triggered(bool);
61+
void on_actionLoad_Template_triggered(bool);
62+
void on_actionSave_As_Template_triggered(bool);
6163

6264
void on_actionConstrainX_triggered(bool enabled);
6365
void on_actionConstrainXY_triggered(bool enabled);
@@ -182,6 +184,8 @@ class FredView: public QMainWindow, public IDialogProvider {
182184
private:
183185
bool saveMissionToCurrentPath();
184186
bool saveMissionAs();
187+
void saveAsTemplate();
188+
void loadTemplate();
185189
bool maybePromptToSaveMissionChanges(const QString& actionDescription);
186190
bool isMissionModified() const;
187191

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include "SaveAsTemplateDialog.h"
2+
3+
#include "ui_SaveAsTemplateDialog.h"
4+
5+
#include <QPushButton>
6+
7+
namespace fso::fred::dialogs {
8+
9+
SaveAsTemplateDialog::SaveAsTemplateDialog(QWidget* parent, const SCP_string& defaultAuthor)
10+
: QDialog(parent), ui(new Ui::SaveAsTemplateDialog())
11+
{
12+
ui->setupUi(this);
13+
14+
ui->authorEdit->setText(QString::fromStdString(defaultAuthor));
15+
16+
// Save button starts disabled until a title is entered
17+
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
18+
19+
connect(ui->titleEdit, &QLineEdit::textChanged, this, &SaveAsTemplateDialog::onTitleChanged);
20+
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SaveAsTemplateDialog::onAccepted);
21+
}
22+
23+
SaveAsTemplateDialog::~SaveAsTemplateDialog() = default;
24+
25+
MissionTemplateInfo SaveAsTemplateDialog::templateInfo() const
26+
{
27+
MissionTemplateInfo info;
28+
info.title = ui->titleEdit->text().trimmed().toUtf8().constData();
29+
info.author = ui->authorEdit->text().trimmed().toUtf8().constData();
30+
info.tags = ui->tagsEdit->text().trimmed().toUtf8().constData();
31+
info.description = ui->descriptionEdit->toPlainText().toUtf8().constData();
32+
return info;
33+
}
34+
35+
void SaveAsTemplateDialog::onTitleChanged(const QString& text)
36+
{
37+
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(!text.trimmed().isEmpty());
38+
}
39+
40+
void SaveAsTemplateDialog::onAccepted()
41+
{
42+
accept();
43+
}
44+
45+
} // namespace fso::fred::dialogs

0 commit comments

Comments
 (0)