yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom.cc
Go to the documentation of this file.
1#include "rom.h"
2
3#include <algorithm>
4#include <atomic>
5#include <cerrno>
6#include <chrono>
7#include <cstddef>
8#include <cstdint>
9#include <cstring>
10#include <ctime>
11#include <filesystem>
12#include <fstream>
13#include <iostream>
14#include <new>
15#include <string>
16#include <system_error>
17#include <vector>
18
19#include "absl/cleanup/cleanup.h"
20#include "absl/status/status.h"
21#include "absl/status/statusor.h"
22#include "absl/strings/str_cat.h"
23#include "absl/strings/str_format.h"
24#include "absl/strings/string_view.h"
29#include "rom/snes.h"
30#include "rom/write_fence.h"
31#include "util/hex.h"
32#include "util/log.h"
33#include "util/macro.h"
34
35#ifdef __EMSCRIPTEN__
36#include <emscripten.h>
38#endif
39
40#if defined(_WIN32)
41#include <windows.h>
42#else
43#include <fcntl.h>
44#include <unistd.h>
45#endif
46
47namespace yaze {
48
49namespace {
50
51// ============================================================================
52// Copier Header Detection and Removal
53// ============================================================================
54
56constexpr size_t kCopierHeaderAlignment = 0x8000;
57constexpr size_t kLoRomInternalHeader = 0x7FC0;
58constexpr size_t kHiRomInternalHeader = 0xFFC0;
59
60// A plausible SNES internal header: map mode byte of the form 0x2X and a
61// checksum/complement pair that XORs to 0xFFFF.
62bool HasInternalHeaderAt(const uint8_t* data, size_t size, size_t offset) {
63 if (offset + 0x20 > size) {
64 return false;
65 }
66 const uint8_t map_mode = data[offset + 0x15];
67 const uint16_t complement = data[offset + 0x1C] | (data[offset + 0x1D] << 8);
68 const uint16_t checksum = data[offset + 0x1E] | (data[offset + 0x1F] << 8);
69 return (map_mode & 0xE0) == 0x20 && (complement ^ checksum) == 0xFFFF;
70}
71
72bool HasInternalHeader(const uint8_t* data, size_t size, size_t base) {
73 return HasInternalHeaderAt(data, size, base + kLoRomInternalHeader) ||
74 HasInternalHeaderAt(data, size, base + kHiRomInternalHeader);
75}
76
77void MaybeStripCopierHeader(std::vector<uint8_t>& rom_data,
78 std::vector<uint8_t>& header,
79 CopierHeaderReason& reason) {
80 header.clear();
81 reason = DetectCopierHeader(rom_data.data(), rom_data.size());
82 if (reason == CopierHeaderReason::kNone) {
83 return;
84 }
85 header.assign(rom_data.begin(), rom_data.begin() + kCopierHeaderSize);
86 rom_data.erase(rom_data.begin(), rom_data.begin() + kCopierHeaderSize);
87 LOG_INFO("Rom", "Stripped copier header (%s); ROM size 0x%zX",
88 CopierHeaderReasonName(reason), rom_data.size());
89}
90
91std::string MakeSafeTimestamp(std::time_t now_c) {
92 std::string timestamp = std::ctime(&now_c);
93 timestamp.erase(std::remove(timestamp.begin(), timestamp.end(), '\n'),
94 timestamp.end());
95 std::replace(timestamp.begin(), timestamp.end(), ' ', '_');
96
97 // Keep backup/save-new filenames valid across platforms (especially
98 // Windows, where ':' and several other characters are not allowed).
99 for (char& ch : timestamp) {
100 switch (ch) {
101 case '<':
102 case '>':
103 case ':':
104 case '"':
105 case '/':
106 case '\\':
107 case '|':
108 case '?':
109 case '*':
110 ch = '-';
111 break;
112 default:
113 break;
114 }
115 }
116 return timestamp;
117}
118
119absl::StatusOr<std::filesystem::path> GetAvailableBackupPath(
120 const std::filesystem::path& requested_path) {
121 std::filesystem::path candidate = requested_path;
122 for (int suffix = 1; suffix <= 1000; ++suffix) {
123 std::error_code exists_ec;
124 const bool exists = std::filesystem::exists(candidate, exists_ec);
125 if (exists_ec) {
126 return absl::InternalError(absl::StrCat(
127 "Could not inspect required ROM backup path: ", candidate.string(),
128 ": ", exists_ec.message()));
129 }
130 if (!exists) {
131 return candidate;
132 }
133 candidate = requested_path.string() + "_" + std::to_string(suffix);
134 }
135
136 return absl::ResourceExhaustedError(
137 "Could not allocate a unique required ROM backup path");
138}
139
140#if !defined(__EMSCRIPTEN__)
141void BestEffortFsyncFile(const std::filesystem::path& path);
142void BestEffortFsyncParentDir(const std::filesystem::path& file_path);
143#endif
144
146 const std::filesystem::path& source_path,
147 const std::filesystem::path& requested_backup_path) {
148 auto backup_path_or = GetAvailableBackupPath(requested_backup_path);
149 if (!backup_path_or.ok()) {
150 return backup_path_or.status();
151 }
152
153 const std::filesystem::path backup_path = *backup_path_or;
154 std::filesystem::path temp_path = backup_path;
155 temp_path += ".tmp";
156
157 std::error_code copy_ec;
158 const bool copied = std::filesystem::copy_file(
159 source_path, temp_path, std::filesystem::copy_options::none, copy_ec);
160 if (!copied || copy_ec) {
161 std::error_code cleanup_ec;
162 std::filesystem::remove(temp_path, cleanup_ec);
163 return absl::FailedPreconditionError(absl::StrCat(
164 "Could not create required ROM backup: ", source_path.string(), " -> ",
165 backup_path.string(), ": ",
166 copy_ec ? copy_ec.message() : "copy did not complete"));
167 }
168
169#if !defined(__EMSCRIPTEN__)
170 BestEffortFsyncFile(temp_path);
171#endif
172
173 std::error_code rename_ec;
174 std::filesystem::rename(temp_path, backup_path, rename_ec);
175 if (rename_ec) {
176 std::error_code cleanup_ec;
177 std::filesystem::remove(temp_path, cleanup_ec);
178 return absl::FailedPreconditionError(absl::StrCat(
179 "Could not finalize required ROM backup: ", backup_path.string(), ": ",
180 rename_ec.message()));
181 }
182
183#if !defined(__EMSCRIPTEN__)
184 BestEffortFsyncParentDir(backup_path);
185#endif
186
187 return absl::OkStatus();
188}
189
190// Match the exclusive staging contract used by CLI artifact publication.
191// Keep the name independent of the destination basename so long ROM filenames
192// still fit the filesystem's component limit. Exclusive creation, rather than
193// the nonce alone, establishes ownership; never reopen this path to write it.
194std::atomic<bool> g_fail_rom_staging_for_testing{false};
195
196absl::StatusOr<std::filesystem::path> WriteExclusiveRomTemp(
197 const std::filesystem::path& target_path,
198 const std::vector<uint8_t>& bytes) {
199 if (g_fail_rom_staging_for_testing.load(std::memory_order_relaxed)) {
200 return absl::InternalError(
201 "Could not create temp ROM file: staging failure injected for testing");
202 }
203 static std::atomic<uint64_t> sequence{0};
204 for (int attempt = 0; attempt < 100; ++attempt) {
205 const auto tick = static_cast<uint64_t>(
206 std::chrono::steady_clock::now().time_since_epoch().count());
207 const auto id = sequence.fetch_add(1, std::memory_order_relaxed);
208 const auto temp_path =
209 target_path.parent_path() /
210 absl::StrFormat(".yaze-rom-%016x-%016x.tmp", tick, id);
211#if defined(_WIN32)
212 HANDLE file =
213 CreateFileW(temp_path.wstring().c_str(), GENERIC_WRITE, 0, nullptr,
214 CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
215 if (file == INVALID_HANDLE_VALUE) {
216 const DWORD error = GetLastError();
217 if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
218 continue;
219 }
220 return absl::InternalError(absl::StrCat(
221 "Could not create temp ROM file: ", temp_path.string(), ": ",
222 std::error_code(static_cast<int>(error), std::system_category())
223 .message()));
224 }
225#else
226 int file = open(temp_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0666);
227 if (file < 0) {
228 const int error = errno;
229 if (error == EEXIST) {
230 continue;
231 }
232 return absl::InternalError(absl::StrCat(
233 "Could not create temp ROM file: ", temp_path.string(), ": ",
234 std::error_code(error, std::generic_category()).message()));
235 }
236#endif
237 // Only installed after successful exclusive creation. On failure we may
238 // remove our staging file, never a pre-existing destination or .tmp file.
239 absl::Cleanup cleanup = [&] {
240#if defined(_WIN32)
241 if (file != INVALID_HANDLE_VALUE) {
242 (void)CloseHandle(file);
243 }
244#else
245 if (file >= 0) {
246 (void)close(file);
247 }
248#endif
249 std::error_code ec;
250 std::filesystem::remove(temp_path, ec);
251 };
252
253 size_t offset = 0;
254 while (offset < bytes.size()) {
255 const size_t chunk = std::min<size_t>(bytes.size() - offset, 1024 * 1024);
256#if defined(_WIN32)
257 DWORD written = 0;
258 const bool ok = WriteFile(file, bytes.data() + offset,
259 static_cast<DWORD>(chunk), &written, nullptr);
260 const int error =
261 ok ? ERROR_WRITE_FAULT : static_cast<int>(GetLastError());
262 if (!ok || written == 0) {
263 return absl::InternalError(absl::StrCat(
264 "Error while writing ROM file: ", temp_path.string(), ": ",
265 std::error_code(error, std::system_category()).message()));
266 }
267#else
268 const ssize_t written = write(file, bytes.data() + offset, chunk);
269 if (written < 0 && errno == EINTR) {
270 continue;
271 }
272 if (written <= 0) {
273 const int error = written < 0 ? errno : EIO;
274 return absl::InternalError(absl::StrCat(
275 "Error while writing ROM file: ", temp_path.string(), ": ",
276 std::error_code(error, std::generic_category()).message()));
277 }
278#endif
279 offset += static_cast<size_t>(written);
280 }
281
282#if defined(_WIN32)
283 if (!FlushFileBuffers(file)) {
284 const int error = static_cast<int>(GetLastError());
285 return absl::InternalError(absl::StrCat(
286 "Could not flush temp ROM file: ", temp_path.string(), ": ",
287 std::error_code(error, std::system_category()).message()));
288 }
289 const bool closed = CloseHandle(file);
290 const int close_error = closed ? 0 : static_cast<int>(GetLastError());
291 file = INVALID_HANDLE_VALUE;
292 const auto close_category = &std::system_category();
293#else
294#if !defined(__EMSCRIPTEN__)
295 int flush_result;
296 do {
297 flush_result = fsync(file);
298 } while (flush_result != 0 && errno == EINTR);
299 if (flush_result != 0) {
300 const int error = errno;
301 return absl::InternalError(absl::StrCat(
302 "Could not flush temp ROM file: ", temp_path.string(), ": ",
303 std::error_code(error, std::generic_category()).message()));
304 }
305#endif
306 const int close_result = close(file);
307 const int close_error = close_result == 0 ? 0 : errno;
308 file = -1;
309 const auto close_category = &std::generic_category();
310#endif
311 if (close_error != 0) {
312 return absl::InternalError(absl::StrCat(
313 "Could not close temp ROM file: ", temp_path.string(), ": ",
314 std::error_code(close_error, *close_category).message()));
315 }
316 std::move(cleanup).Cancel();
317 return temp_path;
318 }
319 return absl::ResourceExhaustedError(
320 "Could not allocate a unique temp ROM file");
321}
322
323#ifdef __EMSCRIPTEN__
324inline void MaybeBroadcastChange(uint32_t offset,
325 const std::vector<uint8_t>& old_bytes,
326 const std::vector<uint8_t>& new_bytes) {
327 if (new_bytes.empty())
328 return;
329 auto& collab = app::platform::GetWasmCollaborationInstance();
330 if (!collab.IsConnected() || collab.IsApplyingRemoteChange()) {
331 return;
332 }
333 (void)collab.BroadcastChange(offset, old_bytes, new_bytes);
334}
335#endif
336
337#if !defined(__EMSCRIPTEN__)
338void BestEffortFsyncFile(const std::filesystem::path& path) {
339#if defined(_WIN32)
340 // FlushFileBuffers requires GENERIC_WRITE access.
341 HANDLE handle =
342 CreateFileW(path.wstring().c_str(), GENERIC_WRITE,
343 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
344 nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
345 if (handle == INVALID_HANDLE_VALUE) {
346 return;
347 }
348 (void)FlushFileBuffers(handle);
349 (void)CloseHandle(handle);
350#else
351 int fd = open(path.c_str(), O_RDONLY);
352 if (fd < 0) {
353 return;
354 }
355 (void)fsync(fd);
356 (void)close(fd);
357#endif
358}
359
360void BestEffortFsyncParentDir(const std::filesystem::path& file_path) {
361#if defined(_WIN32)
362 (void)file_path;
363 // Best-effort only; Windows directory fsync is not portable here.
364#else
365 std::filesystem::path dir_path = file_path.parent_path();
366 if (dir_path.empty()) {
367 dir_path = ".";
368 }
369 int fd = open(dir_path.c_str(), O_RDONLY);
370 if (fd < 0) {
371 return;
372 }
373 (void)fsync(fd);
374 (void)close(fd);
375#endif
376}
377#endif // !defined(__EMSCRIPTEN__)
378
379} // namespace
380
381CopierHeaderReason DetectCopierHeader(const uint8_t* data, size_t size) {
382 if (data == nullptr || size <= kCopierHeaderSize) {
384 }
385 const size_t remainder = size % kCopierHeaderAlignment;
386 if (remainder == 0) {
388 }
389 const bool header_in_place = HasInternalHeader(data, size, 0);
390 const bool header_after_skip =
391 HasInternalHeader(data, size, kCopierHeaderSize);
392 if (remainder == kCopierHeaderSize) {
393 // Standard layout. Still keep a headerless image with 0x200 trailing
394 // bytes, recognizable by an internal header valid only in place.
395 return header_in_place && !header_after_skip
398 }
399 // Nonstandard sizes (trailing data after the ROM) need positive evidence.
400 return header_after_skip && !header_in_place
403}
404
406 switch (reason) {
408 return "none";
410 return "size_alignment";
412 return "internal_header_offset";
413 }
414 return "none";
415}
416
418 g_fail_rom_staging_for_testing.store(fail, std::memory_order_relaxed);
419}
420
422 auto result = rom::InspectLyraLayout(rom_data_.data(), rom_data_.size());
424 result.presence == rom::LyraLayoutPresence::kAbsent) {
425 result.presence = rom::LyraLayoutPresence::kInvalid;
426 result.write_supported = false;
427 result.error = "Loaded Lyra descriptor was removed or damaged";
428 }
429 return result;
430}
431
432absl::Status Rom::CheckLayoutWritable() const {
433 if (read_only_scope_depth_ != 0) {
434 return absl::PermissionDeniedError(
435 "ROM write refused by read_only project policy");
436 }
437 const auto layout = InspectLyraLayout();
438 if (layout.presence == rom::LyraLayoutPresence::kAbsent) {
439 return absl::OkStatus();
440 }
441 return absl::FailedPreconditionError(
442 layout.presence == rom::LyraLayoutPresence::kValid
443 ? (layout.a0_terrain_write_supported
444 ? "Lyra A0 terrain requires dedicated Save As; generic save "
445 "refused"
446 : "Lyra overworld ROM layout is inspection-only; save refused")
447 : absl::StrCat("Invalid Lyra overworld ROM layout: ", layout.error,
448 "; save refused"));
449}
450
455 const auto hash = core::ComputeSourceArtifactSha256(std::string_view(
456 reinterpret_cast<const char*>(rom_data_.data()), rom_data_.size()));
457 if (hash != contract.source_rom_sha256)
458 return absl::OkStatus();
459 std::vector<std::pair<uint32_t, uint32_t>> ranges;
460 for (uint32_t base : contract.dictionary_bases_pc)
461 ranges.emplace_back(base, base + 0x67E0);
462 // Native Tile16 definitions, native pointer rows, the supplemental stream
463 // transfer and old A0 diagnostic slots are all immutable in this phase.
464 // Native full-load, packed decoder/relocation operands, and sub-overlay
465 // consumers pinned by the exact source ownership proof.
466 ranges.emplace_back(0x1759D, 0x175D2);
467 ranges.emplace_back(0x17691, 0x177CB);
468 ranges.emplace_back(0x177E3, 0x17818);
469 ranges.emplace_back(0x78000, 0x80000);
470 ranges.emplace_back(0x1E8000, 0x1F0000);
471 ranges.emplace_back(0x132000, 0x132400);
472 ranges.emplace_back(contract.stream_pc_range[0], contract.stream_pc_range[1]);
473 for (uint32_t table : {0x1794Du, 0x17B2Du}) {
474 ranges.emplace_back(table, table + 160 * 3);
475 for (int map = 0; map < 160; ++map) {
476 ASSIGN_OR_RETURN(uint32_t pointer, ReadLong(table + map * 3));
477 const uint32_t pc = SnesToPc(pointer);
478 ASSIGN_OR_RETURN(auto decoded,
480 rom_data_.data(), rom_data_.size(), pc, 256, true));
481 if (decoded.data.size() != 256 ||
482 pc + decoded.compressed_size > rom_data_.size())
483 return absl::FailedPreconditionError(
484 "Private Lyra native stream is invalid");
485 ranges.emplace_back(pc, pc + decoded.compressed_size);
486 }
487 }
488 std::sort(ranges.begin(), ranges.end());
489 std::vector<std::pair<uint32_t, uint32_t>> merged;
490 for (auto range : ranges) {
491 if (!merged.empty() && range.first <= merged.back().second)
492 merged.back().second = std::max(merged.back().second, range.second);
493 else
494 merged.push_back(range);
495 }
496 auto fence = std::make_shared<rom::WriteFence>();
497 std::vector<std::pair<uint32_t, std::vector<uint8_t>>> preimages;
498 uint32_t cursor = 0;
499 for (auto [begin, end] : merged) {
500 if (end > rom_data_.size())
501 return absl::FailedPreconditionError("Private Lyra range outside ROM");
502 if (cursor < begin)
503 RETURN_IF_ERROR(fence->Allow(cursor, begin, "outside private terrain"));
504 preimages.emplace_back(begin,
505 std::vector<uint8_t>(rom_data_.begin() + begin,
506 rom_data_.begin() + end));
507 cursor = end;
508 }
509 if (cursor < rom_data_.size())
511 fence->Allow(cursor, rom_data_.size(), "outside private terrain"));
513 private_lyra_fence_ = std::move(fence);
515 return absl::OkStatus();
516}
517
519 const core::HackManifest& manifest) {
520 if (!manifest.lyra_private_reservation())
522 if (!manifest.loaded())
523 return absl::FailedPreconditionError("Private Lyra manifest is not loaded");
524 if (*manifest.lyra_private_reservation() !=
526 return absl::FailedPreconditionError(
527 "Private Lyra reservation contract mismatch");
532 return absl::FailedPreconditionError(
533 "Private Lyra reservation source ROM hash mismatch");
534 return absl::OkStatus();
535}
536
538 const std::vector<uint8_t>& image) const {
540 return absl::OkStatus();
541 if (image.size() != 0x400000) {
542 return absl::FailedPreconditionError(
543 "Private Lyra exact-save ROM size changed");
544 }
545 for (const auto& [pc, bytes] : private_lyra_preimages_) {
546 if (pc > image.size() || bytes.size() > image.size() - pc ||
547 !std::equal(bytes.begin(), bytes.end(), image.begin() + pc))
548 return absl::FailedPreconditionError(
549 "Private Lyra reservation protected terrain changed; native save "
550 "refused");
551 }
552 return absl::OkStatus();
553}
554
558
559absl::Status Rom::CheckPrivateLyraWrite(uint32_t pc, const uint8_t* bytes,
560 size_t size) const {
561 if (!private_lyra_attached_ || size == 0)
562 return absl::OkStatus();
563 // Check only changed bytes. Exact no-op serializers stay compatible with
564 // existing fences; shared packed nibbles cannot change a protected neighbor.
565 for (size_t i = 0; i < size; ++i) {
566 if (rom_data_[pc + i] != bytes[i])
568 private_lyra_fence_->Check(pc + i, 1, "private native terrain"));
569 }
570 return absl::OkStatus();
571}
572
573Rom::Rom(const Rom& other)
574 : private_lyra_attached_(other.private_lyra_attached_),
575 private_lyra_preimages_(other.private_lyra_preimages_),
576 private_lyra_fence_(other.private_lyra_fence_),
577 size_(other.size_),
578 title_(other.title_),
579 filename_(other.filename_),
580 short_name_(other.short_name_),
581 rom_data_(other.rom_data_),
582 lyra_terrain_baseline_(other.lyra_terrain_baseline_),
583 copier_header_(other.copier_header_),
584 copier_header_reason_(other.copier_header_reason_),
585 lyra_attributed_at_load_(other.lyra_attributed_at_load_),
586 resource_label_manager_(other.resource_label_manager_),
587 dirty_(other.dirty_),
588 object_tile_revision_(other.object_tile_revision_) {
589 // Write fences and read-only guards are instance-local scope state. A new
590 // Rom must not inherit active scopes on `other`, including dry-run copies.
591}
592
593Rom& Rom::operator=(const Rom& other) {
594 if (this == &other) {
595 return *this;
596 }
597
602 }
604 size_ = other.size_;
605 title_ = other.title_;
606 filename_ = other.filename_;
607 short_name_ = other.short_name_;
608 rom_data_ = other.rom_data_;
614 dirty_ = other.dirty_;
615
616 // Preserve this Rom's active write fences and read-only scopes rather than
617 // importing instance-local scope state from `other`.
618
622 return *this;
623}
624
626 // Rom historically has copy-on-rvalue assignment semantics. Keep the source
627 // intact while ensuring in-place session replacement uses the same strictly
628 // monotonic destination generation as lvalue assignment.
629 return operator=(static_cast<const Rom&>(other));
630}
631
632absl::Status Rom::LoadFromFile(const std::string& filename,
633 const LoadOptions& options) {
634 if (filename.empty()) {
635 return absl::InvalidArgumentError(
636 "Could not load ROM: parameter `filename` is empty.");
637 }
638
639 // Attached reloads validate and load one bounded snapshot. Never reopen a
640 // second image after validating the first, and commit file identity last.
642 std::ifstream candidate_file(filename, std::ios::binary | std::ios::ate);
643 if (!candidate_file)
644 return absl::NotFoundError("Could not open ROM: " + filename);
645 const auto length = candidate_file.tellg();
646 if (length < 32768 || length > 16 * 1024 * 1024) {
647 return absl::InvalidArgumentError(
648 "Attached ROM file must be between 32KB and 16MB");
649 }
650 std::vector<uint8_t> candidate(static_cast<size_t>(length));
651 candidate_file.seekg(0, std::ios::beg);
652 if (!candidate_file.read(reinterpret_cast<char*>(candidate.data()),
653 length)) {
654 return absl::DataLossError(
655 "Could not read complete attached ROM snapshot");
656 }
657 RETURN_IF_ERROR(LoadFromData(candidate, options));
658#ifdef __EMSCRIPTEN__
660#else
661 filename_ = std::filesystem::absolute(filename).string();
662#endif
663 short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
664 if (options.load_resource_labels) {
666 absl::StrFormat("%s.labels", filename));
667 }
668 return absl::OkStatus();
669 }
670
671#ifdef __EMSCRIPTEN__
673 std::ifstream test_file(filename_, std::ios::binary);
674 if (!test_file.is_open()) {
675 return absl::NotFoundError(absl::StrCat(
676 "ROM file does not exist or cannot be opened: ", filename_));
677 }
678 test_file.seekg(0, std::ios::end);
679 size_ = test_file.tellg();
680 test_file.close();
681
682 if (size_ < 32768) {
683 return absl::InvalidArgumentError(absl::StrFormat(
684 "ROM file too small (%zu bytes), minimum is 32KB", size_));
685 }
686#else
687 if (!std::filesystem::exists(filename)) {
688 return absl::NotFoundError(
689 absl::StrCat("ROM file does not exist: ", filename));
690 }
691 filename_ = std::filesystem::absolute(filename).string();
692#endif
693 short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
694
695 std::ifstream file(filename_, std::ios::binary);
696 if (!file.is_open()) {
697 return absl::NotFoundError(
698 absl::StrCat("Could not open ROM file: ", filename_));
699 }
700
701#ifndef __EMSCRIPTEN__
702 try {
703 size_ = std::filesystem::file_size(filename_);
704 if (size_ < 32768) {
705 return absl::InvalidArgumentError(absl::StrFormat(
706 "ROM file too small (%zu bytes), minimum is 32KB", size_));
707 }
708 } catch (...) {
709 file.seekg(0, std::ios::end);
710 size_ = file.tellg();
711 }
712#endif
713
714 // ALttP ROMs are <= 4MB (with an SMC header ~4MB+512); reject anything far
715 // larger so a mis-selected file cannot force a huge up-front allocation.
716 constexpr size_t kMaxRomSize = 16 * 1024 * 1024; // 16 MB
717 if (size_ > kMaxRomSize) {
718 return absl::InvalidArgumentError(absl::StrFormat(
719 "ROM file too large (%zu bytes), maximum is 16MB", size_));
720 }
721
722 try {
723 rom_data_.resize(size_);
724 file.seekg(0, std::ios::beg);
725 file.read(reinterpret_cast<char*>(rom_data_.data()), size_);
726 } catch (const std::bad_alloc& e) {
727 return absl::ResourceExhaustedError(absl::StrFormat(
728 "Failed to allocate memory for ROM (%zu bytes)", size_));
729 }
730
731 file.close();
732
733 copier_header_.clear();
735 if (options.strip_header) {
736 MaybeStripCopierHeader(rom_data_, copier_header_, copier_header_reason_);
737 }
738 size_ = rom_data_.size();
739
744
745 if (options.load_resource_labels) {
746 resource_label_manager_.LoadLabels(absl::StrFormat("%s.labels", filename));
747 }
748
749 // Parse SNES Header for Title
750 if (rom_data_.size() >= 0x8000) {
751 // Check LoROM (0x7FC0) vs HiROM (0xFFC0)
752 // Simple heuristic: Z3 is LoROM
753 size_t header_offset = 0x7FC0;
754 if (rom_data_.size() >= 0x10000) {
755 // Compute checksums to verify?
756 // For now default to LoROM
757 }
758
759 if (header_offset + 21 <= rom_data_.size()) {
760 char buffer[22] = {0};
761 for (int i = 0; i < 21; ++i) {
763 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
764 }
765 title_ = std::string(buffer);
766 // Trim trailing spaces safely
767 auto last_non_space = title_.find_last_not_of(' ');
768 if (last_non_space == std::string::npos) {
769 title_.clear();
770 } else {
771 title_.erase(last_non_space + 1);
772 }
773 }
774 }
775
778 return absl::OkStatus();
779}
780
781absl::Status Rom::LoadFromData(const std::vector<uint8_t>& data,
782 const LoadOptions& options) {
783 if (data.empty()) {
784 return absl::InvalidArgumentError(
785 "Could not load ROM: parameter `data` is empty.");
786 }
788 auto candidate = data;
789 std::vector<uint8_t> header;
791 if (options.strip_header)
792 MaybeStripCopierHeader(candidate, header, reason);
794 }
795 rom_data_ = data;
796
797 copier_header_.clear();
799 if (options.strip_header) {
800 MaybeStripCopierHeader(rom_data_, copier_header_, copier_header_reason_);
801 }
802 size_ = rom_data_.size();
803
808
809 // Parse SNES Header for Title
810 if (rom_data_.size() >= 0x8000) {
811 size_t header_offset = 0x7FC0;
812 if (header_offset + 21 <= rom_data_.size()) {
813 char buffer[22] = {0};
814 for (int i = 0; i < 21; ++i) {
816 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
817 }
818 title_ = std::string(buffer);
819 auto last_non_space = title_.find_last_not_of(' ');
820 if (last_non_space == std::string::npos) {
821 title_.clear();
822 } else {
823 title_.erase(last_non_space + 1);
824 }
825 }
826 }
827
830 return absl::OkStatus();
831}
832
835 const auto layout = InspectLyraLayout();
836 if (layout.presence == rom::LyraLayoutPresence::kValid &&
837 layout.a0_terrain_write_supported) {
839 }
840}
841
842absl::Status Rom::SaveLyraA0TerrainAs(const std::array<uint16_t, 256>& tiles,
843 const std::string& filename) {
845 return absl::FailedPreconditionError(
846 "Private Lyra reservation forbids alternate A0 save route");
847 if (read_only_scope_depth_ != 0) {
848 return absl::PermissionDeniedError(
849 "ROM write refused by read_only project policy");
850 }
851#ifdef __EMSCRIPTEN__
852 return absl::UnimplementedError(
853 "Lyra terrain Save As requires native storage");
854#else
855 if (filename.empty() || lyra_terrain_baseline_.empty() ||
857 return absl::FailedPreconditionError(
858 "Lyra terrain Save As requires the unchanged loaded ROM snapshot");
859 }
860 std::error_code path_ec;
861 const auto target = std::filesystem::weakly_canonical(filename, path_ec);
862 if (path_ec) {
863 return absl::InvalidArgumentError("Cannot resolve Lyra Save As target");
864 }
865 if (!filename_.empty()) {
866 const auto source = std::filesystem::weakly_canonical(filename_, path_ec);
867 if (path_ec) {
868 return absl::InvalidArgumentError("Cannot resolve Lyra source ROM path");
869 }
870 const bool equivalent =
871 std::filesystem::equivalent(source, target, path_ec);
872 if (source == target || (!path_ec && equivalent)) {
873 return absl::FailedPreconditionError(
874 "Lyra terrain requires Save As to a different backing file");
875 }
876 }
879 Rom candidate(*this);
880 candidate.rom_data_ = std::move(staged);
881 SaveSettings settings;
882 settings.filename = filename;
883 settings.require_backup = true;
884 RETURN_IF_ERROR(candidate.SaveToFileUnchecked(settings));
885 candidate.filename_ = filename;
886 candidate.short_name_ = std::filesystem::path(filename).filename().string();
887 candidate.CaptureLyraTerrainBaseline();
888 *this = candidate;
889 return absl::OkStatus();
890#endif
891}
892
898
899absl::Status Rom::SaveToFileUnchecked(const SaveSettings& settings) {
901 if (rom_data_.empty()) {
902 return absl::InternalError("ROM data is empty.");
903 }
904
905 std::string filename = settings.filename;
906 if (filename.empty()) {
908 }
909
910 // Backup modes must reason about the actual destination, including the
911 // timestamped path selected by save_new.
912 if (settings.save_new) {
913 auto now = std::chrono::system_clock::now();
914 auto now_c = std::chrono::system_clock::to_time_t(now);
915 auto filename_no_ext = filename.substr(0, filename.find_last_of("."));
916 filename =
917 absl::StrCat(filename_no_ext, "_", MakeSafeTimestamp(now_c), ".sfc");
918 }
919
920 if (settings.require_backup) {
921 const std::filesystem::path target_path(filename);
922 std::error_code exists_ec;
923 const bool target_exists = std::filesystem::exists(target_path, exists_ec);
924 if (exists_ec) {
925 return absl::FailedPreconditionError(absl::StrCat(
926 "Could not inspect required ROM backup target: ", filename, ": ",
927 exists_ec.message()));
928 }
929
930 // A strict backup protects the file that is about to be replaced, not the
931 // ROM's original load path. A new target has no previous bytes to protect.
932 if (target_exists) {
933 auto now = std::chrono::system_clock::now();
934 auto now_c = std::chrono::system_clock::to_time_t(now);
935 std::string backup_filename =
936 absl::StrCat(filename, "_backup_", MakeSafeTimestamp(now_c));
937 RETURN_IF_ERROR(CreateRequiredBackup(target_path, backup_filename));
938 }
939 } else if (settings.backup) {
940 try {
941 const std::filesystem::path target_path(filename);
942 // Best-effort backups protect the file about to be replaced. A new
943 // destination has no previous bytes to preserve.
944 if (std::filesystem::exists(target_path)) {
945 auto now = std::chrono::system_clock::now();
946 auto now_c = std::chrono::system_clock::to_time_t(now);
947 std::string backup_filename =
948 absl::StrCat(filename, "_backup_", MakeSafeTimestamp(now_c));
949 std::filesystem::copy(
950 target_path, backup_filename,
951 std::filesystem::copy_options::overwrite_existing);
952 }
953 } catch (const std::filesystem::filesystem_error& e) {
954 LOG_WARN("Rom", "Could not create backup: %s", e.what());
955 }
956 }
957
958 // A copier header stripped at load goes back in front of the data, so the
959 // file keeps its layout (dropping it would shift every file offset).
960 std::vector<uint8_t> headered;
961 if (!copier_header_.empty()) {
962 headered.reserve(copier_header_.size() + rom_data_.size());
963 headered.insert(headered.end(), copier_header_.begin(),
964 copier_header_.end());
965 headered.insert(headered.end(), rom_data_.begin(), rom_data_.end());
966 }
967 const std::vector<uint8_t>& file_bytes =
969
970 // Write only through an exclusively created staging handle, then replace
971 // the destination. A pre-existing .tmp file/link must never be truncated.
972 const std::filesystem::path target_path(filename);
973 ASSIGN_OR_RETURN(const std::filesystem::path temp_path,
974 WriteExclusiveRomTemp(target_path, file_bytes));
975
976 std::error_code rename_ec;
977 std::filesystem::rename(temp_path, target_path, rename_ec);
978#if defined(_WIN32)
979 // Windows may reject std::filesystem::rename when the destination exists.
980 // Replace it without deleting the original first, so a failed replacement
981 // does not leave the target missing.
982 if (rename_ec) {
983 if (MoveFileExW(temp_path.wstring().c_str(), target_path.wstring().c_str(),
985 rename_ec.clear();
986 } else {
987 rename_ec = std::error_code(static_cast<int>(GetLastError()),
988 std::system_category());
989 }
990 }
991#endif
992 if (rename_ec) {
993 std::error_code rm_ec;
994 std::filesystem::remove(temp_path, rm_ec);
995 return absl::InternalError(absl::StrCat(
996 "Failed to move temp ROM into place: ", rename_ec.message()));
997 }
998
999#if !defined(__EMSCRIPTEN__)
1000 // Best-effort fsync the parent dir so the rename is durable.
1001 BestEffortFsyncParentDir(target_path);
1002#endif
1003
1004 dirty_ = false;
1005 return absl::OkStatus();
1006}
1007
1009 if (fence == nullptr) {
1010 return;
1011 }
1012 write_fence_stack_.push_back(fence);
1013}
1014
1016 if (fence == nullptr) {
1017 return;
1018 }
1019 if (!write_fence_stack_.empty() && write_fence_stack_.back() == fence) {
1020 write_fence_stack_.pop_back();
1021 return;
1022 }
1023
1024 // Defensive: avoid leaving a stale fence active if call sites mismatch.
1025 for (auto it = write_fence_stack_.rbegin(); it != write_fence_stack_.rend();
1026 ++it) {
1027 if (*it == fence) {
1028 write_fence_stack_.erase(std::next(it).base());
1029 LOG_WARN("Rom", "Popped non-top write fence (mismatched scope)");
1030 return;
1031 }
1032 }
1033 LOG_WARN("Rom", "PopWriteFence called for unknown fence");
1034}
1035
1036absl::StatusOr<uint8_t> Rom::ReadByte(int offset) const {
1037 if (offset < 0 || offset >= static_cast<int>(rom_data_.size())) {
1038 return absl::OutOfRangeError(absl::StrFormat(
1039 "Offset %d out of range (size: %d)", offset, rom_data_.size()));
1040 }
1041 return rom_data_[offset];
1042}
1043
1044absl::StatusOr<uint16_t> Rom::ReadWord(int offset) const {
1045 if (offset < 0 || offset + 1 >= static_cast<int>(rom_data_.size())) {
1046 return absl::OutOfRangeError("Offset out of range");
1047 }
1048 return (uint16_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8));
1049}
1050
1051absl::StatusOr<uint32_t> Rom::ReadLong(int offset) const {
1052 if (offset < 0 || offset + 2 >= static_cast<int>(rom_data_.size())) {
1053 return absl::OutOfRangeError("Offset out of range");
1054 }
1055 return (uint32_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8) |
1056 (rom_data_[offset + 2] << 16));
1057}
1058
1059absl::StatusOr<std::vector<uint8_t>> Rom::ReadByteVector(
1060 uint32_t offset, uint32_t length) const {
1061 if (offset + length > static_cast<uint32_t>(rom_data_.size())) {
1062 return absl::OutOfRangeError("Offset and length out of range");
1063 }
1064 std::vector<uint8_t> result;
1065 result.reserve(length);
1066 for (uint32_t i = offset; i < offset + length; i++) {
1067 result.push_back(rom_data_[i]);
1068 }
1069 return result;
1070}
1071
1072absl::StatusOr<gfx::Tile16> Rom::ReadTile16(uint32_t tile16_id,
1073 uint32_t tile16_ptr) {
1074 // Skip 8 bytes per tile.
1075 auto tpos = tile16_ptr + (tile16_id * 0x08);
1076 gfx::Tile16 tile16 = {};
1079 tpos += 2;
1082 tpos += 2;
1085 tpos += 2;
1088 return tile16;
1089}
1090
1091absl::Status Rom::WriteTile16(int tile16_id, uint32_t tile16_ptr,
1092 const gfx::Tile16& tile) {
1093 auto tpos = tile16_ptr + (tile16_id * 0x08);
1095 tpos += 2;
1097 tpos += 2;
1099 tpos += 2;
1101 return absl::OkStatus();
1102}
1103
1104absl::Status Rom::WriteByte(int addr, uint8_t value) {
1106 if (addr < 0 || addr >= static_cast<int>(rom_data_.size())) {
1107 return absl::OutOfRangeError("Address out of range");
1108 }
1109 for (auto* fence : write_fence_stack_) {
1110 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 1, "WriteByte"));
1111 }
1112 RETURN_IF_ERROR(CheckPrivateLyraWrite(addr, &value, 1));
1113 const uint8_t old_val = rom_data_[addr];
1114 rom_data_[addr] = value;
1115 dirty_ = true;
1116#ifdef __EMSCRIPTEN__
1117 MaybeBroadcastChange(addr, {old_val}, {value});
1118#endif
1119 for (auto* fence : write_fence_stack_) {
1120 fence->RecordWrite(static_cast<uint32_t>(addr), 1);
1121 }
1122 return absl::OkStatus();
1123}
1124
1125absl::Status Rom::WriteWord(int addr, uint16_t value) {
1127 if (addr < 0 || addr + 1 >= static_cast<int>(rom_data_.size())) {
1128 return absl::OutOfRangeError("Address out of range");
1129 }
1130 for (auto* fence : write_fence_stack_) {
1131 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 2, "WriteWord"));
1132 }
1133 const uint8_t private_bytes[] = {static_cast<uint8_t>(value),
1134 static_cast<uint8_t>(value >> 8),
1135 static_cast<uint8_t>(value >> 16)};
1137 const uint8_t old0 = rom_data_[addr];
1138 const uint8_t old1 = rom_data_[addr + 1];
1139 rom_data_[addr] = (uint8_t)(value & 0xFF);
1140 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
1141 dirty_ = true;
1142#ifdef __EMSCRIPTEN__
1144 {static_cast<uint8_t>(value & 0xFF),
1145 static_cast<uint8_t>((value >> 8) & 0xFF)});
1146#endif
1147 for (auto* fence : write_fence_stack_) {
1148 fence->RecordWrite(static_cast<uint32_t>(addr), 2);
1149 }
1150 return absl::OkStatus();
1151}
1152
1153absl::Status Rom::WriteShort(int addr, uint16_t value) {
1154 return WriteWord(addr, value);
1155}
1156
1157absl::Status Rom::WriteLong(uint32_t addr, uint32_t value) {
1159 if (addr + 2 >= static_cast<uint32_t>(rom_data_.size())) {
1160 return absl::OutOfRangeError("Address out of range");
1161 }
1162 for (auto* fence : write_fence_stack_) {
1163 RETURN_IF_ERROR(fence->Check(addr, 3, "WriteLong"));
1164 }
1165 const uint8_t private_bytes[] = {static_cast<uint8_t>(value),
1166 static_cast<uint8_t>(value >> 8),
1167 static_cast<uint8_t>(value >> 16)};
1169 const uint8_t old0 = rom_data_[addr];
1170 const uint8_t old1 = rom_data_[addr + 1];
1171 const uint8_t old2 = rom_data_[addr + 2];
1172 rom_data_[addr] = (uint8_t)(value & 0xFF);
1173 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
1174 rom_data_[addr + 2] = (uint8_t)((value >> 16) & 0xFF);
1175 dirty_ = true;
1176#ifdef __EMSCRIPTEN__
1178 {static_cast<uint8_t>(value & 0xFF),
1179 static_cast<uint8_t>((value >> 8) & 0xFF),
1180 static_cast<uint8_t>((value >> 16) & 0xFF)});
1181#endif
1182 for (auto* fence : write_fence_stack_) {
1183 fence->RecordWrite(addr, 3);
1184 }
1185 return absl::OkStatus();
1186}
1187
1188absl::Status Rom::WriteVector(int addr, std::vector<uint8_t> data) {
1190 if (addr < 0) {
1191 return absl::OutOfRangeError("Address out of range");
1192 }
1193 if (addr + static_cast<int>(data.size()) >
1194 static_cast<int>(rom_data_.size())) {
1195 return absl::OutOfRangeError("Address out of range");
1196 }
1197 for (auto* fence : write_fence_stack_) {
1198 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr),
1199 static_cast<uint32_t>(data.size()),
1200 "WriteVector"));
1201 }
1202 RETURN_IF_ERROR(CheckPrivateLyraWrite(addr, data.data(), data.size()));
1203 std::vector<uint8_t> old_data;
1204 old_data.reserve(data.size());
1205 for (int i = 0; i < static_cast<int>(data.size()); i++) {
1206 old_data.push_back(rom_data_[addr + i]);
1207 rom_data_[addr + i] = data[i];
1208 }
1209 dirty_ = true;
1210#ifdef __EMSCRIPTEN__
1211 MaybeBroadcastChange(addr, old_data, data);
1212#endif
1213 for (auto* fence : write_fence_stack_) {
1214 fence->RecordWrite(static_cast<uint32_t>(addr),
1215 static_cast<uint32_t>(data.size()));
1216 }
1217 return absl::OkStatus();
1218}
1219
1220absl::Status Rom::WriteColor(uint32_t address, const gfx::SnesColor& color) {
1221 uint16_t bgr = ((color.snes() >> 10) & 0x1F) | ((color.snes() & 0x1F) << 10) |
1222 (color.snes() & 0x7C00);
1223 return WriteWord(address, bgr);
1224}
1225
1226absl::Status Rom::WriteHelper(const WriteAction& action) {
1227 if (std::holds_alternative<uint8_t>(action.value)) {
1228 return WriteByte(action.address, std::get<uint8_t>(action.value));
1229 } else if (std::holds_alternative<uint16_t>(action.value) ||
1230 std::holds_alternative<short>(action.value)) {
1231 return WriteShort(action.address, std::get<uint16_t>(action.value));
1232 } else if (std::holds_alternative<std::vector<uint8_t>>(action.value)) {
1233 return WriteVector(action.address,
1234 std::get<std::vector<uint8_t>>(action.value));
1235 } else if (std::holds_alternative<gfx::SnesColor>(action.value)) {
1236 return WriteColor(action.address, std::get<gfx::SnesColor>(action.value));
1237 }
1238 return absl::InvalidArgumentError("Invalid write argument type");
1239}
1240
1241} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:1059
void PushWriteFence(rom::WriteFence *fence)
Definition rom.cc:1008
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:632
auto begin()
Definition rom.h:215
std::vector< uint8_t > rom_data_
Definition rom.h:287
absl::Status CheckLayoutWritable() const
Definition rom.cc:432
absl::Status AttachLyraPrivateReservation(const core::HackManifest &manifest)
Definition rom.cc:518
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:1072
absl::Status WriteColor(uint32_t address, const gfx::SnesColor &color)
Definition rom.cc:1220
auto filename() const
Definition rom.h:219
void PopWriteFence(rom::WriteFence *fence)
Definition rom.cc:1015
auto end()
Definition rom.h:216
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:1104
rom::LyraLayoutInspection InspectLyraLayout() const
Definition rom.cc:421
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:1036
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:1091
std::vector< std::pair< uint32_t, std::vector< uint8_t > > > private_lyra_preimages_
Definition rom.h:265
const auto & vector() const
Definition rom.h:217
std::shared_ptr< rom::WriteFence > private_lyra_fence_
Definition rom.h:266
std::vector< uint8_t > copier_header_
Definition rom.h:293
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:1188
std::string title_
Definition rom.h:278
absl::Status CapturePrivateLyraReservation()
Definition rom.cc:451
bool private_lyra_attached_
Definition rom.h:263
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:893
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:1044
static void SetStagingFailureForTesting(bool fail)
Definition rom.cc:417
CopierHeaderReason copier_header_reason_
Definition rom.h:294
Rom()=default
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
std::vector< rom::WriteFence * > write_fence_stack_
Definition rom.h:307
absl::Status SaveToFileUnchecked(const SaveSettings &settings)
Definition rom.cc:899
Rom & operator=(const Rom &other)
Definition rom.cc:593
bool dirty_
Definition rom.h:301
void CaptureLyraTerrainBaseline()
Definition rom.cc:833
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:781
void AdvanceObjectTileRevision()
Definition rom.h:209
std::vector< uint8_t > lyra_terrain_baseline_
Definition rom.h:290
std::string filename_
Definition rom.h:281
unsigned long size_
Definition rom.h:275
absl::Status ValidateLyraPrivateReservation() const
Definition rom.cc:555
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:1153
absl::Status ValidatePrivateLyraImage(const std::vector< uint8_t > &image) const
Definition rom.cc:537
project::ResourceLabelManager resource_label_manager_
Definition rom.h:298
absl::Status SaveLyraA0TerrainAs(const std::array< uint16_t, 256 > &tiles, const std::string &filename)
Definition rom.cc:842
absl::Status CheckPrivateLyraWrite(uint32_t pc, const uint8_t *bytes, size_t size) const
Definition rom.cc:559
size_t read_only_scope_depth_
Definition rom.h:272
std::string short_name_
Definition rom.h:284
uint64_t object_tile_revision_
Definition rom.h:304
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:1125
virtual absl::Status WriteHelper(const WriteAction &action)
Definition rom.cc:1226
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:1157
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:1051
bool lyra_attributed_at_load_
Definition rom.h:295
Loads and queries the hack manifest JSON for yaze-ASM integration.
const std::optional< LyraPrivateReservation > & lyra_private_reservation() const
bool loaded() const
Check if the manifest has been loaded.
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string MakeSafeTimestamp(std::time_t now_c)
Definition rom.cc:91
std::atomic< bool > g_fail_rom_staging_for_testing
Definition rom.cc:194
absl::Status CreateRequiredBackup(const std::filesystem::path &source_path, const std::filesystem::path &requested_backup_path)
Definition rom.cc:145
void BestEffortFsyncFile(const std::filesystem::path &path)
Definition rom.cc:338
constexpr size_t kLoRomInternalHeader
Definition rom.cc:57
bool HasInternalHeaderAt(const uint8_t *data, size_t size, size_t offset)
Definition rom.cc:62
absl::StatusOr< std::filesystem::path > WriteExclusiveRomTemp(const std::filesystem::path &target_path, const std::vector< uint8_t > &bytes)
Definition rom.cc:196
void BestEffortFsyncParentDir(const std::filesystem::path &file_path)
Definition rom.cc:360
constexpr size_t kCopierHeaderAlignment
SNES ROM images are multiples of 32 KB; a copier header adds 0x200.
Definition rom.cc:56
void MaybeStripCopierHeader(std::vector< uint8_t > &rom_data, std::vector< uint8_t > &header, CopierHeaderReason &reason)
Definition rom.cc:77
constexpr size_t kHiRomInternalHeader
Definition rom.cc:58
bool HasInternalHeader(const uint8_t *data, size_t size, size_t base)
Definition rom.cc:72
absl::StatusOr< std::filesystem::path > GetAvailableBackupPath(const std::filesystem::path &requested_path)
Definition rom.cc:119
std::string ComputeSourceArtifactSha256(std::string_view content)
LyraPrivateReservation PrivateLyraExactSaveReservation()
absl::StatusOr< ExactDecodeResult > DecompressExact(const uint8_t *data, size_t data_size, size_t offset, size_t max_output, bool big_endian_copy)
Decodes one LC-LZ2 stream with the semantics of the ALTTP bank-00 decompressor (Decompress at $00:E79...
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
LyraLayoutInspection InspectLyraLayout(const uint8_t *image, size_t size)
absl::StatusOr< std::vector< uint8_t > > SerializeLyraA0Terrain(const std::vector< uint8_t > &baseline, const std::array< uint16_t, 256 > &tiles)
CopierHeaderReason DetectCopierHeader(const uint8_t *data, size_t size)
Definition rom.cc:381
CopierHeaderReason
Why a copier header was recognized at the start of a ROM file.
Definition rom.h:32
constexpr size_t kCopierHeaderSize
Size of the SMC/SWC copier header that some ROM dumps start with.
Definition rom.h:29
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
const char * CopierHeaderReasonName(CopierHeaderReason reason)
Definition rom.cc:405
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:69
std::string filename
Definition rom.h:57
ValueType value
Definition rom.h:167
bool LoadLabels(const std::string &filename)
Definition project.cc:3488