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