Skip to content

Commit 03ea836

Browse files
authored
Qtfred autosave system (scp-fs2open#7463)
* remove legacy undo system from QtFRED * implement timed autosave feature * clang * undo comments change
1 parent 1240d2e commit 03ea836

28 files changed

Lines changed: 215 additions & 233 deletions

code/missioneditor/missionsave.cpp

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
bypass_comment(expected_version " " property); \
8282
} while (false)
8383

84+
8485
int Fred_mission_save::autosave_mission_file(char* pathname)
8586
{
8687
char backup_name[256], name2[256];
@@ -89,9 +90,9 @@ int Fred_mission_save::autosave_mission_file(char* pathname)
8990
auto len = strlen(pathname);
9091
strcpy_s(backup_name, pathname);
9192
strcpy_s(name2, pathname);
92-
sprintf(backup_name + len, ".%.3d", save_config.mission_backup_depth);
93+
sprintf(backup_name + len, ".%.3d", MISSION_BACKUP_DEPTH);
9394
cf_delete(backup_name, CF_TYPE_MISSIONS);
94-
for (i = save_config.mission_backup_depth; i > 1; i--) {
95+
for (i = MISSION_BACKUP_DEPTH; i > 1; i--) {
9596
sprintf(backup_name + len, ".%.3d", i - 1);
9697
sprintf(name2 + len, ".%.3d", i);
9798
cf_rename(backup_name, name2, CF_TYPE_MISSIONS);
@@ -2424,27 +2425,37 @@ int Fred_mission_save::save_mission_file(const char* pathname)
24242425
save_mission_internal(savepath);
24252426

24262427
if (!err) {
2427-
char backup_name[MAX_PATH_LEN];
2428+
if (save_config.create_bak_file) {
2429+
char backup_name[MAX_PATH_LEN];
24282430

2429-
strcpy_s(backup_name, pathname);
2431+
strcpy_s(backup_name, pathname);
24302432

2431-
// drop extension
2432-
auto ext_ch = strrchr(backup_name, '.');
2433-
if (ext_ch != nullptr)
2434-
*ext_ch = 0;
2433+
// drop extension
2434+
auto ext_ch = strrchr(backup_name, '.');
2435+
if (ext_ch != nullptr)
2436+
*ext_ch = 0;
24352437

2436-
strcat_s(backup_name, ".bak");
2438+
strcat_s(backup_name, ".bak");
24372439
#ifdef _WIN32
2438-
cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
2440+
cf_attrib(pathname, 0, FILE_ATTRIBUTE_READONLY, CF_TYPE_MISSIONS);
24392441
#endif
2440-
cf_delete(backup_name, CF_TYPE_MISSIONS);
2441-
cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
2442-
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
2442+
cf_delete(backup_name, CF_TYPE_MISSIONS);
2443+
cf_rename(pathname, backup_name, CF_TYPE_MISSIONS);
2444+
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
2445+
} else {
2446+
cf_rename(savepath, pathname, CF_TYPE_MISSIONS);
2447+
}
24432448
}
24442449

24452450
return err;
24462451
}
24472452

2453+
int Fred_mission_save::save_autosave_file(const char* pathname)
2454+
{
2455+
save_mission_internal(pathname);
2456+
return err;
2457+
}
2458+
24482459
int Fred_mission_save::save_template_file(const char* pathname)
24492460
{
24502461
char savepath[MAX_PATH_LEN];

code/missioneditor/missionsave.h

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#include "ship/shipfx.h"
1010

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

1414
struct sexp_container;
1515

@@ -32,6 +32,7 @@ struct FredSaveConfig {
3232
matrix view_orient{};
3333

3434
bool always_save_display_names = false;
35+
bool create_bak_file = true;
3536

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

4243
MissionFormat save_format = MissionFormat::STANDARD;
4344

44-
int mission_backup_depth = MISSION_BACKUP_DEPTH; // TODO make user configurable
45-
SCP_string mission_backup_name = MISSION_BACKUP_NAME; // TODO make user configurable
46-
4745
MissionTemplateInfo template_info;
4846
};
4947

@@ -66,12 +64,10 @@ class Fred_mission_save {
6664
void set_fred_alt_names(const char (*names)[NAME_LENGTH + 1]) { save_config.fred_alt_names = names; }
6765
void set_fred_callsigns(const char (*callsigns)[NAME_LENGTH + 1]) { save_config.fred_callsigns = callsigns; }
6866
void set_always_save_display_names(bool always) { save_config.always_save_display_names = always; }
69-
void set_mission_backup_depth(int depth) { save_config.mission_backup_depth = depth; }
70-
void set_mission_backup_name(const SCP_string& name) { save_config.mission_backup_name = name; }
71-
67+
void set_create_bak_file(bool create) { save_config.create_bak_file = create; }
7268

7369
/**
74-
* @brief Saves the mission onto the undo stack
70+
* @brief Saves the mission onto the backup stack
7571
*
7672
* @param[in] pathname The full pathname
7773
*
@@ -81,6 +77,8 @@ class Fred_mission_save {
8177
* @returns A negative value if an error occured.
8278
*
8379
* @see save_mission_internal()
80+
*
81+
* @note Used by legacy FRED2 only; QtFRED uses save_autosave_file() instead.
8482
*/
8583
int autosave_mission_file(char* pathname);
8684

@@ -98,6 +96,17 @@ class Fred_mission_save {
9896
*/
9997
int save_mission_file(const char* pathname);
10098

99+
/**
100+
* @brief Saves the mission directly to an absolute path without any .bak rename dance.
101+
* Used by the QtFRED timer-based autosave, which writes to an AppData directory
102+
* outside the game's virtual file system.
103+
*
104+
* @param[in] pathname Absolute path for the autosave file
105+
*
106+
* @returns 0 for no error, or a negative value if an error occurred
107+
*/
108+
int save_autosave_file(const char* pathname);
109+
101110
/**
102111
* @brief Saves a mission template (.fst) to the given full pathname
103112
*

qtfred/src/mission/Editor.cpp

Lines changed: 52 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636

3737
#include "ui/QtGraphicsOperations.h"
3838

39+
#include <QDateTime>
40+
#include <QDir>
41+
#include <QFileInfo>
42+
#include <QStandardPaths>
43+
3944
#include "object.h"
4045
#include "management.h"
4146
#include "util.h"
@@ -114,6 +119,13 @@ Editor::Editor() : currentObject{ -1 }, Shield_sys_teams(Iff_info.size(), Global
114119
// When a mission was loaded we need to notify everyone that the mission has changed
115120
connect(this, &Editor::missionLoaded, this, [this](const std::string&) { missionChanged(); });
116121

122+
_autosaveDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/autosave/";
123+
QDir().mkpath(_autosaveDirectory);
124+
125+
_autosaveTimer = new QTimer(this);
126+
_autosaveTimer->setSingleShot(false);
127+
connect(_autosaveTimer, &QTimer::timeout, this, &Editor::performTimedAutosave);
128+
117129
fredApp->runAfterInit([this]() { initialSetup(); });
118130
}
119131

@@ -134,119 +146,56 @@ void Editor::update() {
134146
}
135147
}
136148

137-
int Editor::autosave(const char* /*desc*/) {
138-
if (autosaveDisabled || !_lastActiveViewport)
139-
return 0;
140-
141-
Fred_mission_save save;
142-
save.set_always_save_display_names(_lastActiveViewport->Always_save_display_names);
143-
save.set_view_pos(_lastActiveViewport->camera.view_pos);
144-
save.set_view_orient(_lastActiveViewport->camera.view_orient);
145-
save.set_fred_alt_names(Fred_alt_names);
146-
save.set_fred_callsigns(Fred_callsigns);
147-
148-
// autosave_mission_file() needs a mutable buffer because it reads but doesn't write through it
149-
char backup_name_buf[] = MISSION_BACKUP_NAME;
150-
if (save.autosave_mission_file(backup_name_buf)) {
151-
undoCount = undoAvailable = 0;
152-
return -1;
153-
}
154-
155-
undoCount++;
156-
checkUndo();
157-
return 0;
158-
}
159-
160-
int Editor::checkUndo() {
161-
undoAvailable = 0;
162-
if (undoCount == 0)
163-
return 0;
149+
void Editor::maybeUseAutosave(std::string& filepath)
150+
{
151+
const QString qpath = QString::fromStdString(filepath);
152+
const QString basename = QFileInfo(qpath).fileName();
153+
const QString autosavePath = _autosaveDirectory + basename;
164154

165-
// Undo is available when Backup.002 exists (Backup.001 is the current, .002 is what we load)
166-
CFileLocation loc = cf_find_file_location("Backup.002", CF_TYPE_MISSIONS);
167-
if (loc.found) {
168-
undoAvailable = 1;
169-
return 1;
170-
}
171-
return 0;
172-
}
155+
const QFileInfo autosaveInfo(autosavePath);
156+
if (!autosaveInfo.exists())
157+
return;
173158

174-
bool Editor::autoload() {
175-
if (!undoAvailable || !_lastActiveViewport)
176-
return false;
159+
const QFileInfo originalInfo(qpath);
160+
if (autosaveInfo.lastModified() <= originalInfo.lastModified())
161+
return;
177162

178-
// Load the previous state from Backup.002
179-
if (!loadMission("Backup.002", MPF_FAST_RELOAD))
180-
return false;
163+
if (_lastActiveViewport == nullptr || _lastActiveViewport->dialogProvider == nullptr)
164+
return;
181165

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

185-
// Rotate backups back one slot: .003->.002, .004->.003, etc, .009->.008
186-
char old_name[256], new_name[256];
187-
for (int i = 1; i < MISSION_BACKUP_DEPTH; i++) {
188-
sprintf(old_name, "Backup.%.3d", i + 1);
189-
sprintf(new_name, "Backup.%.3d", i);
190-
cf_rename(old_name, new_name, CF_TYPE_MISSIONS);
172+
if (result == DialogButton::Yes) {
173+
filepath = autosavePath.toStdString();
191174
}
192-
193-
if (undoCount > 0)
194-
undoCount--;
195-
checkUndo();
196-
return true;
197175
}
198176

199-
void Editor::maybeUseAutosave(std::string& filepath)
200-
{
201-
// first, just grab the info of this mission
202-
if (!parse_main(filepath.c_str(), MPF_ONLY_MISSION_INFO))
203-
return;
204-
SCP_string created = The_mission.created;
205-
CFileLocation res = cf_find_file_location(filepath.c_str(), CF_TYPE_ANY);
206-
time_t modified = res.m_time;
207-
if (!res.found)
208-
{
209-
UNREACHABLE("Couldn't find path '%s' even though parse_main() succeeded!", filepath.c_str());
210-
return;
211-
}
212-
213-
// now check all the autosaves
214-
SCP_string backup_name;
215-
CFileLocation backup_res;
216-
for (int i = 1; i <= MISSION_BACKUP_DEPTH; ++i)
217-
{
218-
backup_name = MISSION_BACKUP_NAME;
219-
char extension[5];
220-
sprintf(extension, ".%.3d", i);
221-
backup_name += extension;
222-
223-
backup_res = cf_find_file_location(backup_name.c_str(), CF_TYPE_MISSIONS);
224-
if (backup_res.found && parse_main(backup_res.full_name.c_str(), MPF_ONLY_MISSION_INFO))
225-
{
226-
SCP_string this_created = The_mission.created;
227-
time_t this_modified = backup_res.m_time;
228-
229-
if (created == this_created && this_modified > modified)
230-
break;
231-
}
177+
void Editor::startAutosaveTimer(int intervalSeconds) {
178+
_autosaveTimer->stop();
179+
if (intervalSeconds > 0)
180+
_autosaveTimer->start(intervalSeconds * 1000);
181+
}
232182

233-
backup_name.clear();
234-
}
183+
void Editor::stopAutosaveTimer() {
184+
_autosaveTimer->stop();
185+
}
235186

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

243-
auto z = _lastActiveViewport->dialogProvider->showButtonDialog(DialogType::Question,
244-
"Recover from autosave",
245-
prompt.c_str(),
246-
{ DialogButton::Yes, DialogButton::No });
247-
if (z == DialogButton::Yes)
248-
filepath = backup_res.full_name; // replace the specified file with the autosave file
191+
void Editor::performTimedAutosave() {
192+
QString savePath;
193+
if (_currentMissionPath.isEmpty()) {
194+
savePath = _autosaveDirectory + "untitled_autosave.fs2";
195+
} else {
196+
savePath = _autosaveDirectory + QFileInfo(_currentMissionPath).fileName();
249197
}
198+
autosaveDue(savePath);
250199
}
251200

252201
bool Editor::loadMission(const std::string& mission_name, int flags) {
@@ -482,8 +431,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) {
482431
}
483432

484433
if (!(flags & MPF_FAST_RELOAD)) {
485-
undoCount = undoAvailable = 0;
486-
autosave("nothing");
434+
// TODO(Phase 3): _undoStack->clear()
487435
}
488436

489437
return true;
@@ -850,8 +798,7 @@ void Editor::createNewMission() {
850798
clearMission();
851799
create_player(&vmd_zero_vector, &vmd_identity_matrix);
852800
stars_post_level_init();
853-
undoCount = undoAvailable = 0;
854-
autosave("nothing");
801+
// TODO(Phase 3): _undoStack->clear()
855802
missionLoaded("");
856803
}
857804
void Editor::hideMarkedObjects() {

0 commit comments

Comments
 (0)