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
37 changes: 24 additions & 13 deletions code/missioneditor/missionsave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
bypass_comment(expected_version " " property); \
} while (false)


int Fred_mission_save::autosave_mission_file(char* pathname)
{
char backup_name[256], name2[256];
Expand All @@ -89,9 +90,9 @@ int Fred_mission_save::autosave_mission_file(char* pathname)
auto len = strlen(pathname);
strcpy_s(backup_name, pathname);
strcpy_s(name2, pathname);
sprintf(backup_name + len, ".%.3d", save_config.mission_backup_depth);
sprintf(backup_name + len, ".%.3d", MISSION_BACKUP_DEPTH);
cf_delete(backup_name, CF_TYPE_MISSIONS);
for (i = save_config.mission_backup_depth; i > 1; i--) {
for (i = MISSION_BACKUP_DEPTH; i > 1; i--) {
sprintf(backup_name + len, ".%.3d", i - 1);
sprintf(name2 + len, ".%.3d", i);
cf_rename(backup_name, name2, CF_TYPE_MISSIONS);
Expand Down Expand Up @@ -2420,27 +2421,37 @@ int Fred_mission_save::save_mission_file(const char* pathname)
save_mission_internal(savepath);

if (!err) {
char backup_name[MAX_PATH_LEN];
if (save_config.create_bak_file) {
char backup_name[MAX_PATH_LEN];

strcpy_s(backup_name, pathname);
strcpy_s(backup_name, pathname);

// drop extension
auto ext_ch = strrchr(backup_name, '.');
if (ext_ch != nullptr)
*ext_ch = 0;
// drop extension
auto ext_ch = strrchr(backup_name, '.');
if (ext_ch != nullptr)
*ext_ch = 0;

strcat_s(backup_name, ".bak");
strcat_s(backup_name, ".bak");
#ifdef _WIN32
cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
#endif
cf_delete(backup_name, CF_TYPE_MISSIONS);
cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
cf_delete(backup_name, CF_TYPE_MISSIONS);
cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
} else {
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
}
}

return err;
}

int Fred_mission_save::save_autosave_file(const char* pathname)
{
save_mission_internal(pathname);
return err;
}

int Fred_mission_save::save_template_file(const char* pathname)
{
char savepath[MAX_PATH_LEN];
Expand Down
32 changes: 16 additions & 16 deletions code/missioneditor/missionsave.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#include "ship/shipfx.h"

#define MISSION_BACKUP_NAME "Backup"
inline constexpr int MISSION_BACKUP_DEPTH = 9; // TODO make user configurable in QtFRED's future settings menu
inline constexpr int MISSION_BACKUP_DEPTH = 9;

struct sexp_container;

Expand All @@ -32,6 +32,7 @@ struct FredSaveConfig {
matrix view_orient{};

bool always_save_display_names = false;
bool create_bak_file = true;

// These are a little strange since mission saving and campaign saving use the same class here
// which may be worth splitting up in the future. For now these will assert if not set when saving
Expand All @@ -41,9 +42,6 @@ struct FredSaveConfig {

MissionFormat save_format = MissionFormat::STANDARD;

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 @@ -66,21 +64,12 @@ class Fred_mission_save {
void set_fred_alt_names(const char (*names)[NAME_LENGTH + 1]) { save_config.fred_alt_names = names; }
void set_fred_callsigns(const char (*callsigns)[NAME_LENGTH + 1]) { save_config.fred_callsigns = callsigns; }
void set_always_save_display_names(bool always) { save_config.always_save_display_names = always; }
void set_mission_backup_depth(int depth) { save_config.mission_backup_depth = depth; }
void set_mission_backup_name(const SCP_string& name) { save_config.mission_backup_name = name; }

void set_create_bak_file(bool create) { save_config.create_bak_file = create; }

/**
* @brief Saves the mission onto the undo stack
*
* @param[in] pathname The full pathname
*
* @details Returns the value of CFred_mission_save::err, which is:
*
* @returns 0 for no error, or
* @returns A negative value if an error occured.
* @brief Saves the mission onto the backup stack (used by legacy FRED2 only).
*
* @see save_mission_internal()
* @deprecated QtFRED no longer uses this. Retained for FRED2 (fred2/freddoc.cpp) compatibility.
*/
int autosave_mission_file(char* pathname);

Expand All @@ -98,6 +87,17 @@ class Fred_mission_save {
*/
int save_mission_file(const char* pathname);

/**
* @brief Saves the mission directly to an absolute path without any .bak rename dance.
* Used by the QtFRED timer-based autosave, which writes to an AppData directory
* outside the game's virtual file system.
*
* @param[in] pathname Absolute path for the autosave file
*
* @returns 0 for no error, or a negative value if an error occurred
*/
int save_autosave_file(const char* pathname);

/**
* @brief Saves a mission template (.fst) to the given full pathname
*
Expand Down
157 changes: 52 additions & 105 deletions qtfred/src/mission/Editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@

#include "ui/QtGraphicsOperations.h"

#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>

#include "object.h"
#include "management.h"
#include "util.h"
Expand Down Expand Up @@ -114,6 +119,13 @@ Editor::Editor() : currentObject{ -1 }, Shield_sys_teams(Iff_info.size(), Global
// When a mission was loaded we need to notify everyone that the mission has changed
connect(this, &Editor::missionLoaded, this, [this](const std::string&) { missionChanged(); });

_autosaveDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/autosave/";
QDir().mkpath(_autosaveDirectory);

_autosaveTimer = new QTimer(this);
_autosaveTimer->setSingleShot(false);
connect(_autosaveTimer, &QTimer::timeout, this, &Editor::performTimedAutosave);

fredApp->runAfterInit([this]() { initialSetup(); });
}

Expand All @@ -134,119 +146,56 @@ void Editor::update() {
}
}

int Editor::autosave(const char* /*desc*/) {
if (autosaveDisabled || !_lastActiveViewport)
return 0;

Fred_mission_save save;
save.set_always_save_display_names(_lastActiveViewport->Always_save_display_names);
save.set_view_pos(_lastActiveViewport->camera.view_pos);
save.set_view_orient(_lastActiveViewport->camera.view_orient);
save.set_fred_alt_names(Fred_alt_names);
save.set_fred_callsigns(Fred_callsigns);

// autosave_mission_file() needs a mutable buffer because it reads but doesn't write through it
char backup_name_buf[] = MISSION_BACKUP_NAME;
if (save.autosave_mission_file(backup_name_buf)) {
undoCount = undoAvailable = 0;
return -1;
}

undoCount++;
checkUndo();
return 0;
}

int Editor::checkUndo() {
undoAvailable = 0;
if (undoCount == 0)
return 0;
void Editor::maybeUseAutosave(std::string& filepath)
{
const QString qpath = QString::fromStdString(filepath);
const QString basename = QFileInfo(qpath).fileName();
const QString autosavePath = _autosaveDirectory + basename;

// Undo is available when Backup.002 exists (Backup.001 is the current, .002 is what we load)
CFileLocation loc = cf_find_file_location("Backup.002", CF_TYPE_MISSIONS);
if (loc.found) {
undoAvailable = 1;
return 1;
}
return 0;
}
const QFileInfo autosaveInfo(autosavePath);
if (!autosaveInfo.exists())
return;

bool Editor::autoload() {
if (!undoAvailable || !_lastActiveViewport)
return false;
const QFileInfo originalInfo(qpath);
if (autosaveInfo.lastModified() <= originalInfo.lastModified())
return;

// Load the previous state from Backup.002
if (!loadMission("Backup.002", MPF_FAST_RELOAD))
return false;
if (_lastActiveViewport == nullptr || _lastActiveViewport->dialogProvider == nullptr)
return;

// Delete Backup.001 (the state we just replaced)
cf_delete("Backup.001", CF_TYPE_MISSIONS);
const auto result = _lastActiveViewport->dialogProvider->showButtonDialog(
DialogType::Question,
"Autosave Recovery",
"An autosave file for this mission is newer than the original. Load the autosave instead?",
{ DialogButton::Yes, DialogButton::No });

// Rotate backups back one slot: .003->.002, .004->.003, etc, .009->.008
char old_name[256], new_name[256];
for (int i = 1; i < MISSION_BACKUP_DEPTH; i++) {
sprintf(old_name, "Backup.%.3d", i + 1);
sprintf(new_name, "Backup.%.3d", i);
cf_rename(old_name, new_name, CF_TYPE_MISSIONS);
if (result == DialogButton::Yes) {
filepath = autosavePath.toStdString();
}

if (undoCount > 0)
undoCount--;
checkUndo();
return true;
}

void Editor::maybeUseAutosave(std::string& filepath)
{
// first, just grab the info of this mission
if (!parse_main(filepath.c_str(), MPF_ONLY_MISSION_INFO))
return;
SCP_string created = The_mission.created;
CFileLocation res = cf_find_file_location(filepath.c_str(), CF_TYPE_ANY);
time_t modified = res.m_time;
if (!res.found)
{
UNREACHABLE("Couldn't find path '%s' even though parse_main() succeeded!", filepath.c_str());
return;
}

// now check all the autosaves
SCP_string backup_name;
CFileLocation backup_res;
for (int i = 1; i <= MISSION_BACKUP_DEPTH; ++i)
{
backup_name = MISSION_BACKUP_NAME;
char extension[5];
sprintf(extension, ".%.3d", i);
backup_name += extension;

backup_res = cf_find_file_location(backup_name.c_str(), CF_TYPE_MISSIONS);
if (backup_res.found && parse_main(backup_res.full_name.c_str(), MPF_ONLY_MISSION_INFO))
{
SCP_string this_created = The_mission.created;
time_t this_modified = backup_res.m_time;

if (created == this_created && this_modified > modified)
break;
}
void Editor::startAutosaveTimer(int intervalSeconds) {
_autosaveTimer->stop();
if (intervalSeconds > 0)
_autosaveTimer->start(intervalSeconds * 1000);
}

backup_name.clear();
}
void Editor::stopAutosaveTimer() {
_autosaveTimer->stop();
}

// maybe load from the backup instead
if (!backup_name.empty())
{
SCP_string prompt = "Autosaved file ";
prompt += backup_name;
prompt += " has a file modification time more recent than the specified file. Do you want to load the autosave instead?";
void Editor::setCurrentMissionPath(const QString& path) {
_currentMissionPath = path;
}

auto z = _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Question,
"Recover from autosave",
prompt.c_str(),
{ DialogButton::Yes, DialogButton::No });
if (z == DialogButton::Yes)
filepath = backup_res.full_name; // replace the specified file with the autosave file
void Editor::performTimedAutosave() {
QString savePath;
if (_currentMissionPath.isEmpty()) {
savePath = _autosaveDirectory + "untitled_autosave.fs2";
} else {
savePath = _autosaveDirectory + QFileInfo(_currentMissionPath).fileName();
}
autosaveDue(savePath);
}

bool Editor::loadMission(const std::string& mission_name, int flags) {
Expand Down Expand Up @@ -483,8 +432,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) {
}

if (!(flags & MPF_FAST_RELOAD)) {
undoCount = undoAvailable = 0;
autosave("nothing");
// TODO(Phase 3): _undoStack->clear()
}

return true;
Expand Down Expand Up @@ -875,8 +823,7 @@ void Editor::createNewMission() {
clearMission();
create_player(&vmd_zero_vector, &vmd_identity_matrix);
stars_post_level_init();
undoCount = undoAvailable = 0;
autosave("nothing");
// TODO(Phase 3): _undoStack->clear()
missionLoaded("");
}
void Editor::hideMarkedObjects() {
Expand Down
Loading
Loading