Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
filemanagement.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2020 Twilight
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15*/
16
22
23#include <QFileDialog>
24
25#include "./filemanagement.h"
26#include "./savefile.h"
27#include "./savefiletoolset.h"
28//#include "../../../ui/window/mainwindow.h"
29
30FileManagement::FileManagement(QObject* parent) : QObject(parent)
31{
32 data = new SaveFile();
33 reset();
34}
35
37{
38 data->deleteLater();
39}
40
42{
43 return path;
44}
45
47{
48 return recentFiles.at(index);
49}
50
52{
53 return QList<QString>(recentFiles);
54}
55
57{
58 return recentFiles.size();
59}
60
65
66void FileManagement::recentFilesSwap(int from, int to)
67{
68 auto eFrom = recentFiles.at(from);
69 auto eTo = recentFiles.at(to);
70
71 recentFiles.replace(from, eTo);
72 recentFiles.replace(to, eFrom);
73
74 processRecentFileChanges();
75}
76
78{
79 recentFiles.removeAt(ind);
80 processRecentFileChanges();
81}
82
84{
85 setPath("");
86 expandRecentFiles(settings.value(KEY_RECENT_FILES, "").toString());
87 pruneRecentFiles(); // silently drop entries that no longer exist / can't be read
88 newFile();
89}
90
92{
93 setPath("");
94 data->resetData();
95}
96
98{
99 QString file{openFileDialog("Open Save File")};
100 if(file == "")
101 return false;
102
103 // Read + adopt. On failure loadData() surfaces the error and leaves the current
104 // save untouched; don't setPath (so a bad file isn't recorded as recent/current).
105 if(!loadData(file))
106 return false;
107
108 setPath(file);
109 return true;
110}
111
113{
114 QString file{getRecentFile(index)};
115
116 // On failure, leave everything as-is; the error screen is raised by loadData().
117 // Returning false lets the caller (e.g. the New File modal) keep itself open.
118 if(!loadData(file))
119 return false;
120
121 setPath(file);
122 return true;
123}
124
126{
127 // Erase data if path is empty
128 if(path == "") {
129 data->resetData();
130 return;
131 }
132
133 // Otherwise destroy current working copy with copy from disk. On failure the
134 // current working copy is preserved (loadData() leaves the save untouched).
135 loadData(path);
136}
137
138bool FileManagement::loadData(const QString& filePath)
139{
140 var8* newData{readSaveData(filePath)}; // sets lastError; nullptr on failure
141 if(newData == nullptr) {
142 reportLoadError();
143 return false;
144 }
145
146 data->setData(newData); // Copies data out of array (Safe to delete)
147 delete[] newData; // Very important with readSaveData
148 return true;
149}
150
151void FileManagement::reportLoadError()
152{
153 // Plain-English primary message only -- the real technical detail is carried
154 // separately in lastErrorDetail (set by readSaveData) and shown as a secondary line.
155 switch(lastError) {
156 case LoadErrorCannotOpen:
157 lastErrorMessage =
158 "This save file couldn't be opened.\n\n"
159 "It may be open in another program, or you may not have permission to "
160 "read it. Check that the file still exists and try again.";
161 break;
162
163 case LoadErrorTooShort:
164 lastErrorMessage =
165 "This save file looks truncated or corrupted.\n\n"
166 "It's too small to be a valid save, so nothing was loaded and the "
167 "current file was left untouched.";
168 break;
169
170 default:
171 lastErrorMessage = "This save file couldn't be loaded.";
172 break;
173 }
174
175 loadError();
176}
177
179{
180 return lastErrorMessage;
181}
182
184{
185 return lastErrorDetail;
186}
187
189{
190 // Add to top
191 recentFiles.prepend(path);
192
193 // Process (This ensures everyhtign is formatted and cleaned up as expected)
194 processRecentFileChanges();
195}
196
197void FileManagement::setPath(QString path)
198{
199 // Stop here if they're the same
200 if(path == this->path)
201 return;
202
203 //Change paths, notify, add to recentFiles
204 QString oldPath{this->path};
205 this->path = path;
206 pathChanged(path, oldPath);
207
208 // Go no further if path is empty, we don't need to save it
209 if(path == "")
210 return;
211
212 addRecentFile(path);
213 settings.setValue(KEY_LAST_FILE, path);
214}
215
217{
218 if(path == "") {
219 return saveFileAs();
220 }
221
222 data->flattenData();
223 writeSaveData(path, data->data);
224 return true;
225}
226
228{
229 QString filename{saveFileDialog("Save File As...")};
230 if(filename == "")
231 return false;
232
233 data->flattenData();
234 writeSaveData(filename, data->data);
235 setPath(filename);
236 return true;
237}
238
240{
241 QString filename{saveFileDialog("Save Copy As...")};
242 if(filename == "")
243 return false;
244
245 data->flattenData();
246 writeSaveData(filename, data->data);
247 return true;
248}
249
251{
252 data->resetData(true);
253}
254
256{
257 recentFiles.clear();
258 processRecentFileChanges();
259}
260
261void FileManagement::processRecentFileChanges()
262{
263 // Cleanup First make sure correct length and contains no
264 // empty strings or strings with spaces or duplicate strings, etc...
265 QList<QString> newList;
266
267 for(var8 i{0}; i < recentFiles.size(); ++i) {
268 QString file{recentFiles.at(i)};
269 file = file.trimmed();
270 if(file == "" || newList.contains(file))
271 continue;
272
273 newList.append(file);
274 // Cap at exactly MAX_RECENT_FILES. `>=` (not `>`) so we stop after the Nth
275 // entry is appended; `>` kept MAX+1, which leaked one extra path into the
276 // QML recent list (mainwindow's menu already bounded itself at MAX). The
277 // extra slot was NOT a sentinel/terminator -- nothing reads recentFiles[MAX].
278 if(newList.size() >= MAX_RECENT_FILES)
279 break;
280 }
281
282 // Replace current list with newly formatted list
283 recentFiles = newList;
284
285 // Save
286 QString compacted{newList.join(';')};
287 settings.setValue(KEY_RECENT_FILES, compacted);
288
289 // Notify
290 recentFilesChanged(recentFiles);
291}
292
293void FileManagement::pruneRecentFiles()
294{
295 // Startup cleanup: silently drop any recent entry we can't even open for
296 // reading (moved/deleted/renamed/permission). Intentional UX -- a gone file
297 // just disappears from the list; if the user goes looking for it they'll have
298 // to re-open it deliberately. Truncated-but-openable files are NOT removed
299 // here; those surface a clear error only when actually opened.
300 // (Named "prune", not "scrub" -- "scrub" already means wiping a save's unused
301 // bytes in this app; see wipeUnusedSpace.)
302 QList<QString> kept;
303
304 for(const QString& file : recentFiles) {
305 QFile probe(file);
306 if(probe.open(QIODevice::ReadOnly)) {
307 probe.close();
308 kept.append(file);
309 }
310 }
311
312 recentFiles = kept;
313 processRecentFileChanges(); // re-format, persist, and notify
314}
315
316QString FileManagement::openFileDialog(QString title)
317{
318 QString curPath{path};
319
320 if(curPath == "")
321 curPath = settings.value(KEY_LAST_FILE, "").toString();
322
323 return QFileDialog::getOpenFileName(
324 nullptr,
325 //(QWidget*)MainWindow::getInstance(), @TODO
326 title,
327 curPath,
328 "Save Files (*.sav);;All Files (*)");
329}
330
331QString FileManagement::saveFileDialog(QString title)
332{
333 QString curPath{path};
334
335 if(curPath == "")
336 curPath = settings.value(KEY_LAST_FILE, "").toString();
337
338 return QFileDialog::getSaveFileName(
339 nullptr,
340 //(QWidget*)MainWindow::getInstance(), @TODO
341 title,
342 curPath,
343 "Save Files (*.sav);;All Files (*)");
344}
345
346var8* FileManagement::readSaveData(QString filePath)
347{
348 lastError = LoadErrorNone;
349 lastErrorDetail.clear();
350
351 // Load up file in system
352 QFile file(filePath);
353 if(!file.open(QIODevice::ReadOnly)) {
354 lastError = LoadErrorCannotOpen;
355 lastErrorDetail = file.errorString(); // real, one-line OS/Qt reason
356 return nullptr;
357 }
358
359 // Must be AT LEAST a full save's worth of bytes -- not exactly. Larger files
360 // are fine (only the first SAV_DATA_SIZE bytes are the Gen 1 save); anything
361 // shorter is truncated / corrupted and is rejected before we read it.
362 const qint64 fileSize{file.size()};
363 if(fileSize < static_cast<qint64>(SAV_DATA_SIZE)) {
364 lastError = LoadErrorTooShort;
365 lastErrorDetail = QStringLiteral("File is %1 bytes; a save must be at least %2 bytes (32 KB).")
366 .arg(fileSize)
367 .arg(static_cast<qint64>(SAV_DATA_SIZE));
368 file.close();
369 return nullptr;
370 }
371
372 QDataStream in(&file);
373
374 // Allocate and zero-fill first: defensive, so the buffer is never uninitialized
375 // heap garbage even if a read somehow comes up short (a byte-fidelity hazard).
376 char* rawSaveData{new char[SAV_DATA_SIZE]};
377 memset(rawSaveData, 0, SAV_DATA_SIZE);
378
379 // Read the first SAV_DATA_SIZE bytes (the save); ignore any trailing bytes.
380 in.readRawData(rawSaveData, SAV_DATA_SIZE);
381
382 file.close();
383
384 return reinterpret_cast<var8*>(rawSaveData);
385}
386
387void FileManagement::writeSaveData(QString filePath, var8* data)
388{
389 // Re-issue checksums
390 this->data->toolset->recalcChecksums();
391
392 // Load up file in system
393 QFile file(filePath);
394 if(!file.open(QIODevice::WriteOnly))
395 return;
396 QDataStream out(&file);
397
398 // Convert pointer over to a type needed for QDataStream and write file
399 char* dataChar{reinterpret_cast<char*>(data)};
400 out.writeRawData(dataChar, SAV_DATA_SIZE);
401
402 file.close();
403}
404
405void FileManagement::expandRecentFiles(QString files)
406{
407 // Break apart string into paths
408 // Manually add them in, otherwise they oddly get out of order
409 QStringList recentFiles{files.split(';')};
410
411 for(var8 i{0}; i < recentFiles.size(); ++i) {
412 this->recentFiles.append(recentFiles[i]);
413 }
414
415 // Process, cleanup, and notify
416 processRecentFileChanges();
417}
void setPath(QString path)
Set the active path (backs the path property).
void newFile()
Start a fresh blank save.
int recentFilesMax()
The cap (MAX_RECENT_FILES).
QString getLastErrorDetail()
void clearRecentFiles()
Forget the entire recent-files list.
SaveFile * data
The live save file these operations load into / save from.
QString getLastErrorMessage()
bool saveFileAs()
Prompt for a new path and save there.
void recentFilesRemove(int ind)
Drop one entry from the recent list.
QList< QString > getRecentFiles()
The whole recent-files list.
protected::void pathChanged(QString newPath, QString oldPath)
The active path changed.
int recentFilesCount()
How many recent files are currently remembered.
void recentFilesChanged(QList< QString > files)
The recent-files list changed.
QString getRecentFile(int index=0)
Recent path at index (0 = most recent).
void addRecentFile(QString path)
Push a path onto the recent list (de-duped, capped).
virtual ~FileManagement()
void loadError()
A load failed on a file that exists (unreadable / truncated).
FileManagement(QObject *parent=nullptr)
Current file path. Setting it triggers a load via setPath().
void wipeUnusedSpace()
Zero out save regions that aren't meaningfully used.
bool openFileRecent(int index)
Open an entry from the recent-files list.
void reset()
Clear path + data back to a blank starting state.
void reopenFile()
Reload the current path from disk, discarding edits.
bool saveFile()
Save to the current path.
void recentFilesSwap(int from, int to)
Reorder the recent list (e.g. drag to reorder).
bool saveFileCopy()
Save a copy elsewhere without changing the active path.
QString getPath()
Current file path.
bool openFile()
Prompt for and open a save.
One loaded save: the raw 32 KB bytes, their expanded object tree, and the tools that move between the...
Definition savefile.h:46
var8e var8
Everyday 8-bit alias. Exact (not "fastest") to dodge the pointer-width bug noted above.
Definition types.h:124
constexpr const char * KEY_LAST_FILE
QSettings key for the most-recent path.
constexpr var8 MAX_RECENT_FILES
How many recent paths to remember.
constexpr const char * KEY_RECENT_FILES
QSettings key for the recent-files list.
constexpr var16 SAV_DATA_SIZE
Size of a Gen 1 save in bytes (32 KB).
Definition savefile.h:29