yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_file_manager.cc
Go to the documentation of this file.
1#include "rom_file_manager.h"
2
3#include <algorithm>
4#include <chrono>
5#include <ctime>
6#include <filesystem>
7#include <fstream>
8#include <unordered_set>
9
10#include "absl/strings/str_format.h"
12#include "rom/rom.h"
13#include "util/file_util.h"
14#include "util/log.h"
15#include "zelda3/game_data.h"
16
17namespace yaze::editor {
18
19namespace {
20
21std::time_t ToTimeT(const std::filesystem::file_time_type& ftime) {
22 using namespace std::chrono;
23 auto sctp = time_point_cast<system_clock::duration>(
24 ftime - std::filesystem::file_time_type::clock::now() +
25 system_clock::now());
26 return system_clock::to_time_t(sctp);
27}
28
29std::string DayKey(std::time_t timestamp) {
30 std::tm local_tm{};
31#ifdef _WIN32
32 localtime_s(&local_tm, &timestamp);
33#else
34 localtime_r(&timestamp, &local_tm);
35#endif
36 char buffer[16];
37 std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &local_tm);
38 return std::string(buffer);
39}
40
41} // namespace
42
44 : toast_manager_(toast_manager) {}
45
46absl::Status RomFileManager::LoadRom(Rom* rom, const std::string& filename) {
47 if (!rom) {
48 return absl::InvalidArgumentError("ROM pointer cannot be null");
49 }
50 if (filename.empty()) {
51 return absl::InvalidArgumentError("No filename provided");
52 }
53 return LoadRomFromFile(rom, filename);
54}
55
56absl::Status RomFileManager::SaveRom(Rom* rom) {
57 if (!IsRomLoaded(rom)) {
58 return absl::FailedPreconditionError("No ROM loaded to save");
59 }
60
62 auto backup_status = CreateBackup(rom);
63 if (!backup_status.ok()) {
64 if (toast_manager_) {
66 absl::StrFormat("Backup failed: %s", backup_status.message()),
68 }
69 return backup_status;
70 }
71 }
72
73 Rom::SaveSettings settings;
74 settings.backup = false;
75 settings.save_new = false;
76
77 auto status = rom->SaveToFile(settings);
78 if (!status.ok() && toast_manager_) {
80 absl::StrFormat("Failed to save ROM: %s", status.message()),
82 } else if (toast_manager_) {
83 toast_manager_->Show("ROM saved successfully", ToastType::kSuccess);
84 }
85 return status;
86}
87
88absl::Status RomFileManager::SaveRomAs(Rom* rom, const std::string& filename) {
89 if (!IsRomLoaded(rom)) {
90 return absl::FailedPreconditionError("No ROM loaded to save");
91 }
92 if (filename.empty()) {
93 return absl::InvalidArgumentError("No filename provided for save as");
94 }
95
97 auto backup_status = CreateBackup(rom);
98 if (!backup_status.ok()) {
99 if (toast_manager_) {
101 absl::StrFormat("Backup failed: %s", backup_status.message()),
103 }
104 return backup_status;
105 }
106 }
107
108 Rom::SaveSettings settings;
109 settings.backup = false;
110 settings.save_new = true;
111 settings.filename = filename;
112 // settings.z3_save = true; // Deprecated
113
114 auto status = rom->SaveToFile(settings);
115 if (!status.ok() && toast_manager_) {
117 absl::StrFormat("Failed to save ROM as: %s", status.message()),
119 } else if (toast_manager_) {
120 toast_manager_->Show(absl::StrFormat("ROM saved as: %s", filename),
122 }
123 return status;
124}
125
127 const std::string& filename) {
128 if (!rom) {
129 return absl::InvalidArgumentError("ROM pointer cannot be null");
130 }
131 if (filename.empty()) {
132 return absl::InvalidArgumentError("No filename provided");
133 }
134
135 std::string extension = std::filesystem::path(filename).extension().string();
136
137 if (extension == ".yaze" || extension == ".yazeproj" ||
138 extension == ".zsproj") {
139 return absl::UnimplementedError("Project file loading not yet implemented");
140 }
141
142 return LoadRom(rom, filename);
143}
144
146 if (!IsRomLoaded(rom)) {
147 return absl::FailedPreconditionError("No ROM loaded to backup");
148 }
149
150 const std::string source_filename = rom->filename();
151 if (source_filename.empty()) {
152 return absl::InvalidArgumentError("ROM has no filename to backup");
153 }
154
155 // Safety: create backups from the on-disk ROM file, not the in-memory buffer.
156 // This ensures the backup represents the last saved state before overwrite.
157 std::string backup_filename = GenerateBackupFilename(source_filename);
158
159 std::error_code ec;
160 std::filesystem::copy_file(source_filename, backup_filename,
161 std::filesystem::copy_options::overwrite_existing,
162 ec);
163 if (ec) {
164 auto status = absl::InternalError(
165 absl::StrFormat("Failed to create backup: %s", ec.message()));
166 if (toast_manager_) {
168 absl::StrFormat("Backup failed: %s", status.message()),
170 }
171 return status;
172 }
173
174 if (toast_manager_) {
175 toast_manager_->Show(absl::StrFormat("Backup created: %s", backup_filename),
177 }
178
179 auto prune_status = PruneBackups(source_filename);
180 if (!prune_status.ok()) {
181 LOG_WARN("RomFileManager", "Backup prune failed: %s",
182 prune_status.message().data());
183 }
184
185 return absl::OkStatus();
186}
187
189 if (!IsRomLoaded(rom)) {
190 return absl::FailedPreconditionError("No valid ROM to validate");
191 }
192
193 if (rom->size() < 512 * 1024 || rom->size() > 8 * 1024 * 1024) {
194 return absl::InvalidArgumentError("ROM size is outside expected range");
195 }
196 if (rom->title().empty()) {
197 return absl::InvalidArgumentError("ROM title is empty or invalid");
198 }
199
200 if (toast_manager_) {
201 toast_manager_->Show("ROM validation passed", ToastType::kSuccess);
202 }
203 return absl::OkStatus();
204}
205
207 return rom && rom->is_loaded();
208}
209
210std::string RomFileManager::GetRomFilename(Rom* rom) const {
211 if (!IsRomLoaded(rom)) {
212 return "";
213 }
214 return rom->filename();
215}
216
218 const std::string& filename) {
219 if (!rom) {
220 return absl::InvalidArgumentError("ROM pointer cannot be null");
221 }
222 if (!IsValidRomFile(filename)) {
223 return absl::InvalidArgumentError(
224 absl::StrFormat("Invalid ROM file: %s", filename));
225 }
226
227 auto status = rom->LoadFromFile(filename);
228 if (!status.ok()) {
229 if (toast_manager_) {
231 absl::StrFormat("Failed to load ROM: %s", status.message()),
233 }
234 return status;
235 }
236
237 // IMPORTANT: Game data loading is now decoupled and should be handled
238 // by the caller (EditorManager) or a higher-level orchestration layer
239 // that manages both the generic Rom and the Zelda3::GameData.
240 // This class is strictly for generic ROM file operations.
241
242 if (toast_manager_) {
243 toast_manager_->Show(absl::StrFormat("ROM loaded: %s", rom->title()),
245 }
246 return absl::OkStatus();
247}
248
250 const std::string& original_filename) const {
251 std::filesystem::path path(original_filename);
252 std::string stem = path.stem().string();
253 std::string extension = path.extension().string();
254
255 auto now = std::chrono::system_clock::now();
256 auto time_t = std::chrono::system_clock::to_time_t(now);
257 const auto ms_since_epoch =
258 std::chrono::duration_cast<std::chrono::milliseconds>(
259 now.time_since_epoch())
260 .count();
261 const auto ms_part = ms_since_epoch % 1000;
262
263 std::filesystem::path backup_dir = GetBackupDirectory(original_filename);
264
265 std::string filename =
266 absl::StrFormat("%s_backup_%lld_%03lld%s", stem,
267 static_cast<long long>(time_t),
268 static_cast<long long>(ms_part), extension);
269 return (backup_dir / filename).string();
270}
271
273 const std::string& original_filename) const {
274 std::filesystem::path path(original_filename);
275 std::filesystem::path backup_dir = path.parent_path();
276 if (!backup_folder_.empty()) {
277 backup_dir = std::filesystem::path(backup_folder_);
278 }
279 std::error_code ec;
280 std::filesystem::create_directories(backup_dir, ec);
281 return backup_dir;
282}
283
284std::vector<RomFileManager::BackupEntry> RomFileManager::ListBackups(
285 const std::string& rom_filename) const {
286 std::vector<BackupEntry> backups;
287 if (rom_filename.empty()) {
288 return backups;
289 }
290
291 std::filesystem::path rom_path(rom_filename);
292 const std::string stem = rom_path.stem().string();
293 const std::string extension = rom_path.extension().string();
294 const std::string prefix = stem + "_backup_";
295 std::filesystem::path backup_dir = GetBackupDirectory(rom_filename);
296
297 std::error_code ec;
298 for (const auto& entry : std::filesystem::directory_iterator(backup_dir, ec)) {
299 if (ec || !entry.is_regular_file()) {
300 continue;
301 }
302 const auto path = entry.path();
303 if (path.extension() != extension) {
304 continue;
305 }
306 const std::string filename = path.filename().string();
307 if (filename.rfind(prefix, 0) != 0) {
308 continue;
309 }
310
311 BackupEntry backup;
312 backup.path = path.string();
313 backup.filename = filename;
314 backup.size_bytes = entry.file_size(ec);
315 auto ftime = entry.last_write_time(ec);
316 if (!ec) {
317 backup.timestamp = ToTimeT(ftime);
318 }
319 backups.push_back(std::move(backup));
320 }
321
322 std::sort(backups.begin(), backups.end(),
323 [](const BackupEntry& a, const BackupEntry& b) {
324 return a.timestamp > b.timestamp;
325 });
326 return backups;
327}
328
330 const std::string& rom_filename) const {
331 if (backup_retention_count_ <= 0) {
332 return absl::OkStatus();
333 }
334
335 auto backups = ListBackups(rom_filename);
336 if (backups.size() <= static_cast<size_t>(backup_retention_count_)) {
337 return absl::OkStatus();
338 }
339
340 std::unordered_set<std::string> keep_paths;
341 const auto now = std::chrono::system_clock::now();
342 const auto keep_daily_days =
343 std::chrono::hours(24 * std::max(1, backup_keep_daily_days_));
344
345 if (backup_keep_daily_) {
346 std::unordered_set<std::string> seen_days;
347 for (const auto& backup : backups) {
348 if (backup.timestamp == 0) {
349 continue;
350 }
351 const auto backup_time =
352 std::chrono::system_clock::from_time_t(backup.timestamp);
353 if (now - backup_time > keep_daily_days) {
354 continue;
355 }
356 std::string day = DayKey(backup.timestamp);
357 if (seen_days.insert(day).second) {
358 keep_paths.insert(backup.path);
359 }
360 }
361 }
362
363 for (size_t i = 0; i < backups.size() &&
364 keep_paths.size() < static_cast<size_t>(backup_retention_count_);
365 ++i) {
366 keep_paths.insert(backups[i].path);
367 }
368
369 for (const auto& backup : backups) {
370 if (keep_paths.count(backup.path) > 0) {
371 continue;
372 }
373 std::error_code remove_ec;
374 std::filesystem::remove(backup.path, remove_ec);
375 if (remove_ec) {
376 LOG_WARN("RomFileManager", "Failed to delete backup: %s",
377 backup.path.c_str());
378 }
379 }
380
381 return absl::OkStatus();
382}
383
384bool RomFileManager::IsValidRomFile(const std::string& filename) const {
385 if (filename.empty()) {
386 return false;
387 }
388
389 std::error_code ec;
390 if (!std::filesystem::exists(filename, ec) || ec) {
391 return false;
392 }
393
394 auto file_size = std::filesystem::file_size(filename, ec);
395 if (ec) {
396 return false;
397 }
398 // Zelda 3 ROMs are 1MB (0x100000 = 1,048,576 bytes), possibly with 512-byte
399 // SMC header. Allow ROMs from 512KB to 8MB to be safe.
400 if (file_size < 512 * 1024 || file_size > 8 * 1024 * 1024) {
401 return false;
402 }
403
404 return true;
405}
406
407} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:155
auto filename() const
Definition rom.h:145
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:291
auto size() const
Definition rom.h:138
bool is_loaded() const
Definition rom.h:132
auto title() const
Definition rom.h:137
std::string GenerateBackupFilename(const std::string &original_filename) const
absl::Status PruneBackups(const std::string &rom_filename) const
absl::Status OpenRomOrProject(Rom *rom, const std::string &filename)
std::vector< BackupEntry > ListBackups(const std::string &rom_filename) const
absl::Status ValidateRom(Rom *rom)
RomFileManager(ToastManager *toast_manager)
std::string GetRomFilename(Rom *rom) const
absl::Status CreateBackup(Rom *rom)
absl::Status LoadRom(Rom *rom, const std::string &filename)
absl::Status SaveRom(Rom *rom)
absl::Status LoadRomFromFile(Rom *rom, const std::string &filename)
bool IsValidRomFile(const std::string &filename) const
absl::Status SaveRomAs(Rom *rom, const std::string &filename)
bool IsRomLoaded(Rom *rom) const
std::filesystem::path GetBackupDirectory(const std::string &original_filename) const
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
#define LOG_WARN(category, format,...)
Definition log.h:107
std::time_t ToTimeT(const std::filesystem::file_time_type &ftime)
Editors are the view controllers for the application.
std::string filename
Definition rom.h:33