Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/mission/missionparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ extern bool check_for_25_1_data();
#define MPF_ONLY_MISSION_INFO (1 << 0)
#define MPF_IMPORT_FSM (1 << 1)
#define MPF_FAST_RELOAD (1 << 2) // skip clearing some stuff so we can load the mission faster (usually since it's the same mission)
#define MPF_IS_TEMPLATE (1 << 3) // loading a .fst mission template; post-load reset of name, author, timestamps, notes, description, and camera

// bitfield definitions for missions game types
#define OLD_MAX_GAME_TYPES 4 // needed for compatibility
Expand Down
40 changes: 40 additions & 0 deletions code/missioneditor/missionsave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,43 @@ int Fred_mission_save::save_mission_file(const char* pathname)
return err;
}

int Fred_mission_save::save_template_file(const char* pathname)
{
char savepath[MAX_PATH_LEN];

strcpy_s(savepath, "");
auto p = strrchr(pathname, DIR_SEPARATOR_CHAR);
if (p) {
auto len = p - pathname;
strncpy(savepath, pathname, len);
savepath[len] = '\0';
strcat_s(savepath, DIR_SEPARATOR_STR);
}
strcat_s(savepath, "saving.xxx");

save_mission_internal(savepath);

if (!err) {
// Templates don't get .bak backups; just overwrite directly
cf_delete(pathname, CF_TYPE_MISSIONS);
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
}

return err;
}

void Fred_mission_save::save_template_info()
{
const auto& ti = save_config.template_info;

fout("#Template Info\n");
fout("\n$Template Title: %s", ti.title.c_str());
fout("\n$Template Author: %s", ti.author.c_str());
fout("\n$Template Tags: %s", ti.tags.c_str());
fout("\n$Template Description:\n%s\n$end_template_desc", ti.description.c_str());
fout("\n\n#End Template Info\n\n");
}

int Fred_mission_save::save_mission_info()
{
required_string_fred("#Mission Info");
Expand Down Expand Up @@ -3065,6 +3102,9 @@ void Fred_mission_save::save_mission_internal(const char* pathname)
return;
}

if (!save_config.template_info.title.empty())
save_template_info();

// Goober5000
convert_special_tags_to_retail();

Expand Down
30 changes: 30 additions & 0 deletions code/missioneditor/missionsave.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ enum class MissionFormat {
COMPATIBILITY_MODE = 2
};

struct MissionTemplateInfo {
SCP_string title;
SCP_string author;
SCP_string tags;
SCP_string description;
};

struct FredSaveConfig {

vec3d view_pos{};
Expand All @@ -36,6 +43,8 @@ struct FredSaveConfig {

int mission_backup_depth = MISSION_BACKUP_DEPTH; // TODO make user configurable
SCP_string mission_backup_name = MISSION_BACKUP_NAME; // TODO make user configurable

MissionTemplateInfo template_info;
};

/**
Expand All @@ -51,6 +60,7 @@ class Fred_mission_save {
Fred_mission_save() = default;

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

/**
* @brief Saves a mission template (.fst) to the given full pathname
*
* @param[in] pathname The full pathname to save to
*
* @details Saves a complete mission file and prepends a #Template Info section
* containing display metadata (title, author, tags, description) from
* save_config.template_info. The metadata is used by the template browser
* and is ignored by the normal mission parser.
*
* @returns 0 for no error, or
* @returns A negative value if an error occurred
*/
int save_template_file(const char* pathname);

protected:

FredSaveConfig save_config{};
Expand Down Expand Up @@ -168,6 +193,11 @@ class Fred_mission_save {
*/
void save_ai_goals(ai_goal* goalp, int ship);

/**
* @brief Writes the #Template Info section to the top of the file
*/
void save_template_info();

/**
* @brief Saves the skybox bitmaps
*
Expand Down
6 changes: 6 additions & 0 deletions qtfred/source_groups.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,16 @@ add_file_folder("Source/UI/Dialogs"
src/ui/dialogs/ReinforcementsEditorDialog.h
src/ui/dialogs/RelativeCoordinatesDialog.cpp
src/ui/dialogs/RelativeCoordinatesDialog.h
src/ui/dialogs/SaveAsTemplateDialog.cpp
src/ui/dialogs/SaveAsTemplateDialog.h
src/ui/dialogs/SelectionDialog.cpp
src/ui/dialogs/SelectionDialog.h
src/ui/dialogs/ShieldSystemDialog.h
src/ui/dialogs/ShieldSystemDialog.cpp
src/ui/dialogs/TeamLoadoutDialog.cpp
src/ui/dialogs/TeamLoadoutDialog.h
src/ui/dialogs/TemplateBrowserDialog.cpp
src/ui/dialogs/TemplateBrowserDialog.h
src/ui/dialogs/VariableDialog.cpp
src/ui/dialogs/VariableDialog.h
src/ui/dialogs/VoiceActingManager.h
Expand Down Expand Up @@ -333,6 +337,8 @@ add_file_folder("UI"
ui/ShipWeaponsDialog.ui
ui/VariableDialog.ui
ui/WingEditorDialog.ui
ui/SaveAsTemplateDialog.ui
ui/TemplateBrowserDialog.ui
)

add_file_folder("Resources"
Expand Down
21 changes: 21 additions & 0 deletions qtfred/src/mission/Editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,27 @@ bool Editor::loadMission(const std::string& mission_name, int flags) {
viewport->view_orient = Parse_viewer_orient;
viewport->view_pos = Parse_viewer_pos;
}

if (flags & MPF_IS_TEMPLATE) {
// reset fields that should not carry over from the template source
strcpy_s(The_mission.name, "Untitled");
The_mission.author = getUsername();

time_t currentTime;
time(&currentTime);
auto timeinfo = localtime(&currentTime);
time_to_mission_info_string(timeinfo, The_mission.created, DATE_TIME_LENGTH - 1);
strcpy_s(The_mission.modified, The_mission.created);

strcpy_s(The_mission.notes, "This is a FRED2_OPEN created mission.");
strcpy_s(The_mission.mission_desc, "Put mission description here");

for (auto& viewport : _viewports) {
viewport->resetView();
viewport->resetViewPhysics();
}
}

stars_post_level_init();

missionLoaded(filepath);
Expand Down
77 changes: 74 additions & 3 deletions qtfred/src/ui/FredView.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "FredView.h"
#include "ui_FredView.h"

#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
Expand Down Expand Up @@ -42,11 +43,14 @@
#include <ui/dialogs/MusicPlayerDialog.h>
#include <ui/dialogs/RelativeCoordinatesDialog.h>
#include <ui/dialogs/ControlsDialog.h>
#include <ui/dialogs/SaveAsTemplateDialog.h>
#include <ui/dialogs/TemplateBrowserDialog.h>
#include <ui/ControlBindings.h>
#include <iff_defs/iff_defs.h>

#include "mission/Editor.h"
#include "mission/management.h"
#include "mission/missionparse.h"
#include "missioneditor/missionsave.h"

#include "widgets/ColorComboBox.h"
Expand Down Expand Up @@ -170,7 +174,7 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) {
connect(this, &FredView::viewIdle, this, [this]() { ui->actionError_Checker_Checks_Potential_Issues->setChecked(_viewport->Error_checker_checks_potential_issues); });
}

void FredView::loadMissionFile(const QString& pathName) {
void FredView::loadMissionFile(const QString& pathName, int flags) {
if (!maybePromptToSaveMissionChanges(tr("loading another mission"))) {
return;
}
Expand All @@ -183,9 +187,10 @@ void FredView::loadMissionFile(const QString& pathName) {
fred->clean_up_selections();

auto pathToLoad = pathName.toStdString();
fred->maybeUseAutosave(pathToLoad);
if (!(flags & MPF_IS_TEMPLATE))
fred->maybeUseAutosave(pathToLoad);

fred->loadMission(pathToLoad);
fred->loadMission(pathToLoad, flags);

QApplication::restoreOverrideCursor();
} catch (const fso::fred::mission_load_error&) {
Expand Down Expand Up @@ -272,6 +277,68 @@ bool FredView::saveMissionAs() {
return true;
}

void FredView::saveAsTemplate() {
// Collect template metadata first
dialogs::SaveAsTemplateDialog metaDialog(this, getUsername());
if (metaDialog.exec() != QDialog::Accepted)
return;

// Default to data/missions/templates/ and create it if needed
QString templatesDir = QDir::currentPath() + "/data/missions/templates";
QDir().mkpath(templatesDir);

QString templateName = QFileDialog::getSaveFileName(this,
tr("Save As Template"),
templatesDir,
tr("FS2 mission templates (*.fst)"));

if (templateName.isEmpty())
return;

if (!templateName.endsWith(".fst", Qt::CaseInsensitive))
templateName += ".fst";

Fred_mission_save save;
save.set_always_save_display_names(_viewport->Always_save_display_names);
save.set_fred_alt_names(Fred_alt_names);
save.set_fred_callsigns(Fred_callsigns);
save.set_template_info(metaDialog.templateInfo());

save.save_template_file(templateName.replace('/', DIR_SEPARATOR_CHAR).toUtf8().constData());
}

void FredView::loadTemplate() {
QString templatesDir = QDir::currentPath() + "/data/missions/templates";
QDir().mkpath(templatesDir);

dialogs::TemplateBrowserDialog browser(this, templatesDir);
if (browser.exec() != QDialog::Accepted)
return;

QString templateName = browser.selectedTemplatePath();
if (templateName.isEmpty())
return;

auto result = QMessageBox::question(this,
tr("Load Template"),
tr("This will replace all mission data. Continue?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);

if (result != QMessageBox::Yes)
return;

loadMissionFile(templateName.replace('/', DIR_SEPARATOR_CHAR), MPF_IS_TEMPLATE);
}

void FredView::on_actionLoad_Template_triggered(bool) {
loadTemplate();
}

void FredView::on_actionSave_As_Template_triggered(bool) {
saveAsTemplate();
}

void FredView::on_mission_loaded(const std::string& filepath) {
QString filename = "Untitled";
if (!filepath.empty()) {
Expand Down Expand Up @@ -303,6 +370,10 @@ void FredView::newMission() {
fred->createNewMission();
}
void FredView::addToRecentFiles(const QString& path) {
// Templates are not mission files; don't pollute the recent list with them
if (path.endsWith(".fst", Qt::CaseInsensitive))
return;

// First get the list of existing files
QSettings settings;
auto recentFiles = settings.value("FredView/recentFiles").toStringList();
Expand Down
6 changes: 5 additions & 1 deletion qtfred/src/ui/FredView.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class FredView: public QMainWindow, public IDialogProvider {
~FredView() override;
void setEditor(Editor* editor, EditorViewport* viewport);

void loadMissionFile(const QString& pathName);
void loadMissionFile(const QString& pathName, int flags = 0);

QSurface* getRenderSurface();
RenderWidget* getRenderWidget();
Expand All @@ -58,6 +58,8 @@ class FredView: public QMainWindow, public IDialogProvider {
void on_actionSave_As_triggered(bool);
void on_actionSave_triggered(bool);
void on_actionExit_triggered(bool);
void on_actionLoad_Template_triggered(bool);
void on_actionSave_As_Template_triggered(bool);

void on_actionConstrainX_triggered(bool enabled);
void on_actionConstrainXY_triggered(bool enabled);
Expand Down Expand Up @@ -181,6 +183,8 @@ class FredView: public QMainWindow, public IDialogProvider {
private:
bool saveMissionToCurrentPath();
bool saveMissionAs();
void saveAsTemplate();
void loadTemplate();
bool maybePromptToSaveMissionChanges(const QString& actionDescription);
bool isMissionModified() const;

Expand Down
45 changes: 45 additions & 0 deletions qtfred/src/ui/dialogs/SaveAsTemplateDialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "SaveAsTemplateDialog.h"

#include "ui_SaveAsTemplateDialog.h"

#include <QPushButton>

namespace fso::fred::dialogs {

SaveAsTemplateDialog::SaveAsTemplateDialog(QWidget* parent, const SCP_string& defaultAuthor)
: QDialog(parent), ui(new Ui::SaveAsTemplateDialog())
{
ui->setupUi(this);

ui->authorEdit->setText(QString::fromStdString(defaultAuthor));

// Save button starts disabled until a title is entered
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);

connect(ui->titleEdit, &QLineEdit::textChanged, this, &SaveAsTemplateDialog::onTitleChanged);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SaveAsTemplateDialog::onAccepted);
}

SaveAsTemplateDialog::~SaveAsTemplateDialog() = default;

MissionTemplateInfo SaveAsTemplateDialog::templateInfo() const
{
MissionTemplateInfo info;
info.title = ui->titleEdit->text().trimmed().toUtf8().constData();
info.author = ui->authorEdit->text().trimmed().toUtf8().constData();
info.tags = ui->tagsEdit->text().trimmed().toUtf8().constData();
info.description = ui->descriptionEdit->toPlainText().toUtf8().constData();
return info;
}

void SaveAsTemplateDialog::onTitleChanged(const QString& text)
{
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(!text.trimmed().isEmpty());
}

void SaveAsTemplateDialog::onAccepted()
{
accept();
}

} // namespace fso::fred::dialogs
Loading
Loading