yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_old.cc
Go to the documentation of this file.
1#include "rom.h"
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cstddef>
7#include <cstdint>
8#include <cstring>
9#include <ctime>
10#include <filesystem>
11#include <fstream>
12#include <iostream>
13#include <new>
14#include <string>
15#include <vector>
16
17#include "absl/status/status.h"
18#include "absl/status/statusor.h"
19#include "absl/strings/str_cat.h"
20#include "absl/strings/str_format.h"
21#include "absl/strings/string_view.h"
22#include "app/gfx/core/bitmap.h"
27#ifdef __EMSCRIPTEN__
28#include <emscripten.h>
30#endif
31#include "rom/snes.h"
32#include "core/features.h"
33#include "util/hex.h"
34#include "util/log.h"
35#include "util/macro.h"
36#include "zelda.h"
37
38#ifdef __EMSCRIPTEN__
39#include <emscripten.h>
41#endif
42
43namespace yaze {
44
45// ============================================================================
46// Graphics Constants
47// ============================================================================
48
51constexpr int Uncompressed3BPPSize = 0x0600;
52
53namespace {
54
55// ============================================================================
56// ROM Structure Constants
57// ============================================================================
58
60constexpr size_t kBaseRomSize = 1048576;
61
66constexpr size_t kHeaderSize = 0x200; // 512 bytes
67
68// ============================================================================
69// SMC Header Detection and Removal
70// ============================================================================
71
95void MaybeStripSmcHeader(std::vector<uint8_t>& rom_data, unsigned long& size) {
96 if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize &&
97 rom_data.size() >= kHeaderSize) {
98 rom_data.erase(rom_data.begin(), rom_data.begin() + kHeaderSize);
99 size -= kHeaderSize;
100 LOG_INFO("Rom", "Stripped SMC header from ROM (new size: %lu)", size);
101 }
102}
103
104#ifdef __EMSCRIPTEN__
105inline void MaybeBroadcastChange(uint32_t offset,
106 const std::vector<uint8_t>& old_bytes,
107 const std::vector<uint8_t>& new_bytes) {
108 if (new_bytes.empty()) return;
109 auto& collab = app::platform::GetWasmCollaborationInstance();
110 if (!collab.IsConnected() || collab.IsApplyingRemoteChange()) {
111 return;
112 }
113 (void)collab.BroadcastChange(offset, old_bytes, new_bytes);
114}
115#endif
116
117} // namespace
118
122
124 RomLoadOptions options;
125 options.populate_palettes = false;
126 options.populate_gfx_groups = false;
127 options.expand_to_full_image = false;
128 options.load_resource_labels = false;
129 return options;
130}
131
133 RomLoadOptions options;
134 options.load_zelda3_content = false;
135 options.strip_header = false;
136 options.populate_metadata = false;
137 options.populate_palettes = false;
138 options.populate_gfx_groups = false;
139 options.expand_to_full_image = false;
140 options.load_resource_labels = false;
141 return options;
142}
143
144// ============================================================================
145// Graphics Address Resolution
146// ============================================================================
147
175uint32_t GetGraphicsAddress(const uint8_t* data, uint8_t addr, uint32_t ptr1,
176 uint32_t ptr2, uint32_t ptr3, size_t rom_size) {
177 // Bounds check: ensure all pointer table accesses are within ROM bounds
178 // This prevents WASM "index out of bounds" errors when assertions are enabled
179 // Also check for integer overflow before accessing arrays
180 if (ptr1 > UINT32_MAX - addr || ptr1 + addr >= rom_size ||
181 ptr2 > UINT32_MAX - addr || ptr2 + addr >= rom_size ||
182 ptr3 > UINT32_MAX - addr || ptr3 + addr >= rom_size) {
183 // Return rom_size as sentinel value (callers check offset < rom.size())
184 return static_cast<uint32_t>(rom_size);
185 }
186 return SnesToPc(AddressFromBytes(data[ptr1 + addr], data[ptr2 + addr],
187 data[ptr3 + addr]));
188}
189
190// ============================================================================
191// 2BPP Graphics Loading
192// ============================================================================
193
206absl::StatusOr<std::vector<uint8_t>> Load2BppGraphics(const Rom& rom) {
207 std::vector<uint8_t> sheet;
208 const uint8_t sheets[] = {0x71, 0x72, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE};
209 for (const auto& sheet_id : sheets) {
210 auto offset = GetGraphicsAddress(rom.data(), sheet_id,
214 rom.size());
215
216 if (offset >= rom.size()) {
217 return absl::OutOfRangeError(
218 absl::StrFormat("2BPP graphics sheet %u offset %u exceeds ROM size %zu",
219 sheet_id, offset, rom.size()));
220 }
221
222 // Decompress using LC-LZ2 algorithm with 0x800 byte output buffer.
223 // IMPORTANT: The size parameter (0x800) must NOT be 0, or DecompressV2
224 // returns an empty vector immediately. This was a regression bug.
225 ASSIGN_OR_RETURN(auto decomp_sheet,
226 gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size()));
227 auto converted_sheet = gfx::SnesTo8bppSheet(decomp_sheet, 2);
228 for (const auto& each_pixel : converted_sheet) {
229 sheet.push_back(each_pixel);
230 }
231 }
232 return sheet;
233}
234
235absl::StatusOr<std::array<gfx::Bitmap, kNumLinkSheets>> LoadLinkGraphics(
236 const Rom& rom) {
237 const uint32_t kLinkGfxOffset = 0x80000; // $10:8000
238 const uint16_t kLinkGfxLength = 0x800; // 0x4000 or 0x7000?
239 std::array<gfx::Bitmap, kNumLinkSheets> link_graphics;
240 for (uint32_t i = 0; i < kNumLinkSheets; i++) {
242 auto link_sheet_data,
243 rom.ReadByteVector(/*offset=*/kLinkGfxOffset + (i * kLinkGfxLength),
244 /*length=*/kLinkGfxLength));
245 auto link_sheet_8bpp = gfx::SnesTo8bppSheet(link_sheet_data, /*bpp=*/4);
246 link_graphics[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
247 gfx::kTilesheetDepth, link_sheet_8bpp);
248 link_graphics[i].SetPalette(rom.palette_group().armors[0]);
249 // Texture creation is deferred until GraphicsEditor is opened and renderer
250 // is available. The graphics will be queued for texturing when needed via
251 // Arena's deferred system.
252 }
253 return link_graphics;
254}
255
256absl::StatusOr<gfx::Bitmap> LoadFontGraphics(const Rom& rom) {
257 // Validate ROM size before accessing font graphics
258 constexpr uint32_t kFontGraphicsOffset = 0x70000;
259 constexpr uint32_t kFontGraphicsSize = 0x2000;
260 constexpr uint32_t kMinRomSizeForFont = kFontGraphicsOffset + kFontGraphicsSize;
261
262 if (rom.size() < kMinRomSizeForFont) {
263 return absl::FailedPreconditionError(
264 absl::StrFormat("ROM too small for font graphics: %zu bytes (need at least %u bytes)",
265 rom.size(), kMinRomSizeForFont));
266 }
267
268 // Use memcpy instead of byte-by-byte copy for performance
269 std::vector<uint8_t> data(rom.data() + 0x70000, rom.data() + 0x70000 + 0x2000);
270
271 std::vector<uint8_t> new_data(0x4000);
272 std::vector<uint8_t> mask = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
273 int sheet_position = 0;
274
275 // 8x8 tile
276 for (int s = 0; s < 4; s++) { // Per Sheet
277 for (int j = 0; j < 4; j++) { // Per Tile Line Y
278 for (int i = 0; i < 16; i++) { // Per Tile Line X
279 for (int y = 0; y < 8; y++) { // Per Pixel Line
280 uint8_t line_bits0 =
281 data[(y * 2) + (i * 16) + (j * 256) + sheet_position];
282 uint8_t line_bits1 =
283 data[(y * 2) + (i * 16) + (j * 256) + 1 + sheet_position];
284
285 for (int x = 0; x < 4; x++) { // Per Pixel X
286 uint8_t pixdata = 0;
287 uint8_t pixdata2 = 0;
288
289 if ((line_bits0 & mask[(x * 2)]) == mask[(x * 2)]) {
290 pixdata += 1;
291 }
292 if ((line_bits1 & mask[(x * 2)]) == mask[(x * 2)]) {
293 pixdata += 2;
294 }
295
296 if ((line_bits0 & mask[(x * 2) + 1]) == mask[(x * 2) + 1]) {
297 pixdata2 += 1;
298 }
299 if ((line_bits1 & mask[(x * 2) + 1]) == mask[(x * 2) + 1]) {
300 pixdata2 += 2;
301 }
302
303 new_data[(y * 64) + (x) + (i * 4) + (j * 512) + (s * 2048)] =
304 (uint8_t)((pixdata << 4) | pixdata2);
305 }
306 }
307 }
308 }
309
310 sheet_position += 0x400;
311 }
312
313 // Use move semantics to avoid redundant copy
314 gfx::Bitmap font_gfx;
315 font_gfx.Create(128, 128, 64, std::move(new_data));
316 return font_gfx;
317}
318
319// ============================================================================
320// Main Graphics Loading
321// ============================================================================
322
352absl::StatusOr<std::array<gfx::Bitmap, kNumGfxSheets>> LoadAllGraphicsData(
353 Rom& rom, bool defer_render) {
354#ifdef __EMSCRIPTEN__
355 EM_ASM({ console.log('[C++] LoadAllGraphicsData START - ROM size:', $0); }, rom.size());
356#endif
357
358 std::array<gfx::Bitmap, kNumGfxSheets> graphics_sheets;
359 std::vector<uint8_t> sheet;
360 bool bpp3 = false;
361
362 // Clear graphics buffer to prevent corruption from multiple ROM loads
363 rom.mutable_graphics_buffer()->clear();
364 LOG_DEBUG("Graphics", "Cleared graphics buffer, loading %d sheets",
366
367 // -------------------------------------------------------------------------
368 // Diagnostic Logging: ROM Alignment Verification
369 // -------------------------------------------------------------------------
370 // These logs help diagnose issues where the ROM header wasn't stripped
371 // correctly, causing all pointer lookups to be offset by 512 bytes.
372 LOG_INFO("Graphics", "ROM size: %zu bytes (0x%zX)", rom.size(), rom.size());
373 LOG_INFO("Graphics", "Pointer tables: ptr1=0x%X, ptr2=0x%X, ptr3=0x%X",
377
378 // Initialize Diagnostics
379 auto& diag = rom.GetMutableDiagnostics();
380 diag.rom_size = rom.size();
381 // SMC header detection: size % 1MB == 512 indicates header present
382 diag.header_stripped = (rom.size() % (1024 * 1024) != 512);
383 // Checksum calculation
384 if (rom.size() > 0x7FDE) {
385 uint16_t c = rom.data()[0x7FDC] | (rom.data()[0x7FDD] << 8);
386 uint16_t k = rom.data()[0x7FDE] | (rom.data()[0x7FDF] << 8);
387 diag.checksum_valid = ((c ^ k) == 0xFFFF);
388 }
389 diag.ptr1_loc = rom.version_constants().kOverworldGfxPtr1;
390 diag.ptr2_loc = rom.version_constants().kOverworldGfxPtr2;
391 diag.ptr3_loc = rom.version_constants().kOverworldGfxPtr3;
392
393 // Probe first 5 sheets to verify pointer tables are reading valid addresses
394 for (int probe = 0; probe < 5; probe++) {
395 uint32_t ptr1 = rom.version_constants().kOverworldGfxPtr1;
396 uint32_t ptr2 = rom.version_constants().kOverworldGfxPtr2;
397 uint32_t ptr3 = rom.version_constants().kOverworldGfxPtr3;
398 if (ptr1 + probe < rom.size() && ptr2 + probe < rom.size() && ptr3 + probe < rom.size()) {
399 // Pointer tables: ptr1=bank, ptr2=high, ptr3=low (matches GetGraphicsAddress)
400 uint8_t bank = rom.data()[ptr1 + probe];
401 uint8_t high = rom.data()[ptr2 + probe];
402 uint8_t low = rom.data()[ptr3 + probe];
403 uint32_t snes_addr = AddressFromBytes(bank, high, low);
404 uint32_t pc_addr = SnesToPc(snes_addr);
405 LOG_INFO("Graphics", "Sheet %d: bytes=[%02X,%02X,%02X] -> SNES=0x%06X -> PC=0x%X (valid=%s)",
406 probe, bank, high, low, snes_addr, pc_addr,
407 pc_addr < rom.size() ? "yes" : "NO");
408 }
409 }
410
411#ifdef __EMSCRIPTEN__
412 // Start progress tracking for WASM builds
413 auto loading_handle =
414 app::platform::WasmLoadingManager::BeginLoading("Loading Graphics");
415 app::platform::WasmLoadingManager::SetArenaHandle(loading_handle);
416#endif
417
418 for (uint32_t i = 0; i < kNumGfxSheets; i++) {
419#ifdef __EMSCRIPTEN__
420 // Update progress and check for cancellation
421 float progress = static_cast<float>(i) / static_cast<float>(kNumGfxSheets);
422 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, progress);
423 app::platform::WasmLoadingManager::UpdateMessage(
424 loading_handle, absl::StrFormat("Sheet %d/%d", i + 1, kNumGfxSheets));
425
426 // Note: We don't use emscripten_sleep(0) here because it conflicts with
427 // other async operations (like WasmLoadingManager::EndLoading which uses
428 // emscripten_async_call). The UpdateProgress calls already yield to the
429 // browser event loop via the JavaScript interface.
430
431 // Check for user cancellation
432 if (app::platform::WasmLoadingManager::IsCancelled(loading_handle)) {
433 app::platform::WasmLoadingManager::EndLoading(loading_handle);
434 app::platform::WasmLoadingManager::ClearArenaHandle();
435 // Clear partial graphics buffer to prevent corrupted state
436 rom.mutable_graphics_buffer()->clear();
437 return absl::CancelledError("Graphics loading cancelled by user");
438 }
439#endif
440
441 // Diagnostic Capture for this sheet
442 diag.sheets[i].index = i;
443 uint32_t offset = 0;
444 bool offset_valid = false;
445
446 // -----------------------------------------------------------------------
447 // Category 1: Uncompressed 3BPP sheets (115-126)
448 // -----------------------------------------------------------------------
449 // These sheets are stored raw in the ROM without LC-LZ2 compression.
450 // Size is fixed at Uncompressed3BPPSize (0x600 = 1536 bytes).
451 if (i >= 115 && i <= 126) {
452 diag.sheets[i].is_compressed = false;
453 diag.sheets[i].decomp_size_param = Uncompressed3BPPSize;
454
455 offset = GetGraphicsAddress(
459 rom.size());
460
461 diag.sheets[i].pc_offset = offset;
462 diag.sheets[i].snes_address = PcToSnes(offset);
463 offset_valid = (offset < rom.size());
464
465 // Capture first 8 bytes for diagnostics
466 diag.sheets[i].first_bytes.clear();
467 if (offset_valid && offset + 8 <= rom.size()) {
468 for (int b = 0; b < 8; ++b) {
469 diag.sheets[i].first_bytes.push_back(rom.data()[offset + b]);
470 }
471 }
472
473 sheet.resize(Uncompressed3BPPSize);
474
475 if (offset + Uncompressed3BPPSize > rom.size()) {
476 LOG_WARN("Rom", "Uncompressed sheet %u offset 0x%X out of bounds (ROM size 0x%zX)",
477 i, offset, rom.size());
478 // Use zero-filled sheet to maintain buffer alignment
479 sheet.assign(Uncompressed3BPPSize, 0);
480 diag.sheets[i].decompression_succeeded = false;
481 } else {
482 std::copy(rom.data() + offset, rom.data() + offset + Uncompressed3BPPSize,
483 sheet.begin());
484 diag.sheets[i].decompression_succeeded = true;
485 diag.sheets[i].actual_decomp_size = Uncompressed3BPPSize;
486 }
487 bpp3 = true;
488
489 // -----------------------------------------------------------------------
490 // Category 2: 2BPP sheets (113-114, 218+) - Skipped
491 // -----------------------------------------------------------------------
492 // These are loaded separately via Load2BppGraphics(). We still need to
493 // push placeholder data to graphics_buffer to maintain index alignment.
494 } else if (i == 113 || i == 114 || i >= 218) {
495 diag.sheets[i].is_compressed = true; // Typically are compressed
496 // Still capture address for diagnostics
497 offset = GetGraphicsAddress(
501 rom.size());
502 diag.sheets[i].pc_offset = offset;
503 diag.sheets[i].snes_address = PcToSnes(offset);
504 offset_valid = (offset < rom.size());
505
506 // Capture first 8 bytes for diagnostics
507 diag.sheets[i].first_bytes.clear();
508 if (offset_valid && offset + 8 <= rom.size()) {
509 for (int b = 0; b < 8; ++b) {
510 diag.sheets[i].first_bytes.push_back(rom.data()[offset + b]);
511 }
512 }
513
514 bpp3 = false;
515
516 // -----------------------------------------------------------------------
517 // Category 3: Compressed 3BPP sheets (0-112, 127-217)
518 // -----------------------------------------------------------------------
519 // Standard compressed graphics using Nintendo's LC-LZ2 algorithm.
520 } else {
521 diag.sheets[i].is_compressed = true;
522
523 offset = GetGraphicsAddress(
527 rom.size());
528
529 diag.sheets[i].pc_offset = offset;
530 diag.sheets[i].snes_address = PcToSnes(offset);
531 offset_valid = (offset < rom.size());
532
533 // Capture first 8 bytes for diagnostics
534 diag.sheets[i].first_bytes.clear();
535 if (offset_valid && offset + 8 <= rom.size()) {
536 for (int b = 0; b < 8; ++b) {
537 diag.sheets[i].first_bytes.push_back(rom.data()[offset + b]);
538 }
539 }
540
541 if (offset >= rom.size()) {
542 LOG_WARN("Rom", "Compressed sheet %u offset 0x%X exceeds ROM size 0x%zX",
543 i, offset, rom.size());
544 bpp3 = false;
545 diag.sheets[i].decompression_succeeded = false;
546 diag.sheets[i].decomp_size_param = 0x800;
547 } else {
548
549 // Decompress LC-LZ2 data to 0x800 byte buffer
550 // CRITICAL: size parameter MUST be 0x800, not 0!
551 // size=0 causes DecompressV2 to return empty data immediately.
552 diag.sheets[i].decomp_size_param = 0x800;
553 auto decomp_result = gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size());
554 if (decomp_result.ok()) {
555 sheet = *decomp_result;
556 bpp3 = true;
557 diag.sheets[i].decompression_succeeded = true;
558 diag.sheets[i].actual_decomp_size = sheet.size();
559
560 // DEBUG: Log success for specific sheets
561 if (i == 73 || i == 115) {
562 printf("[LoadAllGraphicsData] Sheet %d: Decompressed successfully. Size: %zu. Offset: 0x%X\n",
563 i, sheet.size(), offset);
564 }
565
566 } else {
567 LOG_WARN("Rom", "Decompression failed for sheet %u: %s", i, decomp_result.status().message());
568 // DEBUG: Log failure
569 if (i == 73 || i == 115) {
570 printf("[LoadAllGraphicsData] Sheet %d: Decompression FAILED. Offset: 0x%X\n", i, offset);
571 }
572 bpp3 = false;
573 diag.sheets[i].decompression_succeeded = false;
574 }
575 }
576 }
577
578 // -----------------------------------------------------------------------
579 // Post-processing: Convert and store sheet data
580 // -----------------------------------------------------------------------
581 if (bpp3) {
582 // Convert from SNES 3BPP planar format to linear 8BPP indexed format
583 auto converted_sheet = gfx::SnesTo8bppSheet(sheet, 3);
584
585 // CRITICAL: Enforce 4096-byte size (64 tiles * 64 bytes) for 8BPP sheets
586 // This ensures fixed-stride indexing (sheet_id * 4096) works correctly.
587 // 3BPP decompression might produce slightly more/less data, so we must normalize.
588 if (converted_sheet.size() != 4096) {
589 converted_sheet.resize(4096, 0);
590 }
591
592 // Create bitmap from converted pixel data
593 graphics_sheets[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
594 gfx::kTilesheetDepth, converted_sheet);
595
596 // Apply default palette for immediate visibility (prevents white sheets)
597 if (!rom.palette_group().empty()) {
598 gfx::SnesPalette default_palette;
599
600 if (i < 113) {
601 // Overworld/Dungeon graphics - use dungeon main palette
602 auto palette_group = rom.palette_group().dungeon_main;
603 if (palette_group.size() > 0) {
604 default_palette = palette_group[0];
605 }
606 } else if (i < 128) {
607 // Sprite graphics - use sprite palettes
608 auto palette_group = rom.palette_group().sprites_aux1;
609 if (palette_group.size() > 0) {
610 default_palette = palette_group[0];
611 }
612 } else {
613 // Auxiliary graphics - use HUD/menu palettes
614 auto palette_group = rom.palette_group().hud;
615 if (palette_group.size() > 0) {
616 default_palette = palette_group.palette(0);
617 }
618 }
619
620 if (!default_palette.empty()) {
621 graphics_sheets[i].SetPalette(default_palette);
622 }
623 }
624
625 // Append to legacy graphics buffer for backward compatibility
626 // Use insert with iterators for efficiency instead of byte-by-byte push_back
627 auto& buffer = *rom.mutable_graphics_buffer();
628 const uint8_t* sheet_data = graphics_sheets[i].data();
629 buffer.insert(buffer.end(), sheet_data,
630 sheet_data + graphics_sheets[i].size());
631
632 } else {
633 // Create placeholder bitmap for skipped/failed sheets (2BPP sheets, etc.)
634 // This ensures the bitmap exists even if empty, preventing index out of
635 // bounds errors when editors iterate over all sheets.
636 // 128x32 pixels * 1 byte/pixel = 4096 bytes
637 constexpr size_t kPlaceholderSize = 4096;
638 std::vector<uint8_t> placeholder_data(kPlaceholderSize, 0xFF);
639 graphics_sheets[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
640 gfx::kTilesheetDepth, std::move(placeholder_data));
641
642 // Also append to legacy graphics buffer for backward compatibility
643 // Use resize + memset for efficiency
644 auto& buffer = *rom.mutable_graphics_buffer();
645 size_t old_size = buffer.size();
646 buffer.resize(old_size + kPlaceholderSize, 0xFF);
647 }
648 }
649
650 // Analyze the collected diagnostics
651 diag.Analyze();
652
653#ifdef __EMSCRIPTEN__
654 // Complete progress tracking for WASM builds
655 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 1.0f);
656 app::platform::WasmLoadingManager::EndLoading(loading_handle);
657 app::platform::WasmLoadingManager::ClearArenaHandle();
658#endif
659
660 return graphics_sheets;
661}
662
664 Rom& rom, std::array<gfx::Bitmap, kNumGfxSheets>& gfx_sheets) {
665 for (int i = 0; i < kNumGfxSheets; i++) {
666 if (gfx_sheets[i].is_active()) {
667 int to_bpp = 3;
668 std::vector<uint8_t> final_data;
669 bool compressed = true;
670 if (i >= 115 && i <= 126) {
671 compressed = false;
672 } else if (i == 113 || i == 114 || i >= 218) {
673 to_bpp = 2;
674 continue;
675 }
676
677 std::cout << "Sheet ID " << i << " BPP: " << to_bpp << std::endl;
678 auto sheet_data = gfx_sheets[i].vector();
679 std::cout << "Sheet data size: " << sheet_data.size() << std::endl;
680 final_data = gfx::Bpp8SnesToIndexed(sheet_data, 8);
681 int size = 0;
682 if (compressed) {
683 auto compressed_data = gfx::HyruleMagicCompress(
684 final_data.data(), final_data.size(), &size, 1);
685 for (int j = 0; j < size; j++) {
686 sheet_data[j] = compressed_data[j];
687 }
688 }
689 auto offset = GetGraphicsAddress(
693 rom.size());
694 std::copy(final_data.begin(), final_data.end(), rom.begin() + offset);
695 }
696 }
697 return absl::OkStatus();
698}
699
700absl::Status Rom::LoadFromFile(const std::string& filename, bool z3_load) {
703}
704
705absl::Status Rom::LoadFromFile(const std::string& filename,
706 const RomLoadOptions& options) {
707 if (filename.empty()) {
708 return absl::InvalidArgumentError(
709 "Could not load ROM: parameter `filename` is empty.");
710 }
711
712 // Validate file exists before proceeding
713#ifdef __EMSCRIPTEN__
714 // In Emscripten, use the path as-is (MEMFS paths are absolute from root)
716
717 // Try to open the file to verify it exists (this works with MEMFS)
718 std::ifstream test_file(filename_, std::ios::binary);
719 if (!test_file.is_open()) {
720 return absl::NotFoundError(
721 absl::StrCat("ROM file does not exist or cannot be opened: ", filename_));
722 }
723
724 // Get file size from stream (std::filesystem::file_size doesn't work with MEMFS)
725 test_file.seekg(0, std::ios::end);
726 if (!test_file) {
727 test_file.close();
728 return absl::InternalError("Could not seek to end of ROM file");
729 }
730 size_ = test_file.tellg();
731 test_file.close();
732
733 // Validate ROM size before proceeding
734 if (size_ < 32768) {
735 return absl::InvalidArgumentError(absl::StrFormat(
736 "ROM file too small (%zu bytes), minimum is 32KB", size_));
737 }
738 if (size_ > 8 * 1024 * 1024) {
739 return absl::InvalidArgumentError(absl::StrFormat(
740 "ROM file too large (%zu bytes), maximum is 8MB", size_));
741 }
742#else
743 if (!std::filesystem::exists(filename)) {
744 return absl::NotFoundError(
745 absl::StrCat("ROM file does not exist: ", filename));
746 }
747
748 filename_ = std::filesystem::absolute(filename).string();
749#endif
750 short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
751
752 std::ifstream file(filename_, std::ios::binary);
753 if (!file.is_open()) {
754 return absl::NotFoundError(
755 absl::StrCat("Could not open ROM file: ", filename_));
756 }
757
758 // Get file size and validate
759#ifndef __EMSCRIPTEN__
760 // For non-Emscripten builds, get file size from filesystem
761 try {
762 size_ = std::filesystem::file_size(filename_);
763
764 // Validate ROM size (minimum 32KB, maximum 8MB for expanded ROMs)
765 if (size_ < 32768) {
766 return absl::InvalidArgumentError(absl::StrFormat(
767 "ROM file too small (%zu bytes), minimum is 32KB", size_));
768 }
769 if (size_ > 8 * 1024 * 1024) {
770 return absl::InvalidArgumentError(absl::StrFormat(
771 "ROM file too large (%zu bytes), maximum is 8MB", size_));
772 }
773 } catch (const std::filesystem::filesystem_error& e) {
774 // Try to get the file size from the open file stream
775 file.seekg(0, std::ios::end);
776 if (!file) {
777 return absl::InternalError(absl::StrCat(
778 "Could not get file size: ", filename_, " - ", e.what()));
779 }
780 size_ = file.tellg();
781
782 // Validate size from stream
783 if (size_ < 32768 || size_ > 8 * 1024 * 1024) {
784 return absl::InvalidArgumentError(
785 absl::StrFormat("Invalid ROM size: %zu bytes", size_));
786 }
787 }
788#endif
789 // For Emscripten, size_ was already determined above
790
791 // Allocate and read ROM data
792 try {
793 rom_data_.resize(size_);
794 file.seekg(0, std::ios::beg);
795 file.read(reinterpret_cast<char*>(rom_data_.data()), size_);
796
797 if (!file) {
798 return absl::InternalError(
799 absl::StrFormat("Failed to read ROM data, read %zu of %zu bytes",
800 file.gcount(), size_));
801 }
802 } catch (const std::bad_alloc& e) {
803 return absl::ResourceExhaustedError(absl::StrFormat(
804 "Failed to allocate memory for ROM (%zu bytes)", size_));
805 }
806
807 file.close();
808
809 if (!options.load_zelda3_content) {
810 if (options.strip_header) {
811 MaybeStripSmcHeader(rom_data_, size_);
812 }
813 size_ = rom_data_.size();
814 } else {
815 RETURN_IF_ERROR(LoadZelda3(options));
816 }
817
818 if (options.load_resource_labels) {
819 resource_label_manager_.LoadLabels(absl::StrFormat("%s.labels", filename));
820 }
821
822 return absl::OkStatus();
823}
824
825absl::Status Rom::LoadFromData(const std::vector<uint8_t>& data, bool z3_load) {
828}
829
830absl::Status Rom::LoadFromData(const std::vector<uint8_t>& data,
831 const RomLoadOptions& options) {
832 if (data.empty()) {
833 return absl::InvalidArgumentError(
834 "Could not load ROM: parameter `data` is empty.");
835 }
836 rom_data_ = data;
837 size_ = data.size();
838
839 if (!options.load_zelda3_content) {
840 if (options.strip_header) {
841 MaybeStripSmcHeader(rom_data_, size_);
842 }
843 size_ = rom_data_.size();
844 } else {
845 RETURN_IF_ERROR(LoadZelda3(options));
846 }
847
848 return absl::OkStatus();
849}
850
851absl::Status Rom::LoadZelda3() {
853}
854
855absl::Status Rom::LoadZelda3(const RomLoadOptions& options) {
856 if (rom_data_.empty()) {
857 return absl::FailedPreconditionError("ROM data is empty");
858 }
859
860#ifdef __EMSCRIPTEN__
861 EM_ASM({ console.log('[C++] LoadZelda3 START - size:', $0); }, size_);
862#endif
863
864 LOG_INFO("Rom", "LoadZelda3: Initial size=%lu bytes (0x%lX)", size_, size_);
865
866 if (options.strip_header) {
867 MaybeStripSmcHeader(rom_data_, size_);
868 }
869
870 size_ = rom_data_.size();
871
872 // Log post-strip ROM alignment verification
873 LOG_INFO("Rom", "LoadZelda3: Post-strip size=%zu bytes (0x%zX)", size_, size_);
874
875 // Verify SNES checksum after stripping to confirm alignment
876 if (size_ > 0x7FE0) {
877 uint16_t complement = rom_data_[0x7FDC] | (rom_data_[0x7FDD] << 8);
878 uint16_t checksum = rom_data_[0x7FDE] | (rom_data_[0x7FDF] << 8);
879 bool valid = (complement ^ checksum) == 0xFFFF;
880 LOG_INFO("Rom", "SNES checksum at 0x7FDC: complement=0x%04X checksum=0x%04X XOR=0x%04X valid=%s",
881 complement, checksum, complement ^ checksum, valid ? "YES" : "NO");
882
883 // Log first few bytes at critical pointer table location (kOverworldGfxPtr1 = 0x4F80)
884 if (size_ > 0x4F85) {
885 LOG_INFO("Rom", "Bytes at 0x4F80 (ptr1): %02X %02X %02X %02X %02X %02X",
886 rom_data_[0x4F80], rom_data_[0x4F81], rom_data_[0x4F82],
887 rom_data_[0x4F83], rom_data_[0x4F84], rom_data_[0x4F85]);
888 }
889 }
890
891 constexpr uint32_t kTitleStringOffset = 0x7FC0;
892 constexpr uint32_t kTitleStringLength = 20;
893 constexpr uint32_t kTitleStringOffsetWithHeader = 0x81C0;
894
895 if (options.populate_metadata) {
896 uint32_t offset = options.strip_header ? kTitleStringOffset
898 if (offset + kTitleStringLength > rom_data_.size()) {
899 return absl::OutOfRangeError(
900 "ROM image is too small to contain title metadata.");
901 }
902 title_.assign(rom_data_.begin() + offset,
903 rom_data_.begin() + offset + kTitleStringLength);
904 // Check bounds before accessing version byte (offset + 0x19)
905 if (offset + 0x19 < rom_data_.size()) {
906 if (rom_data_[offset + 0x19] == 0) {
907 version_ = zelda3_version::JP;
908 } else {
909 version_ = zelda3_version::US;
910 }
911 } else {
912 // Default to US if we can't read the version byte
913 version_ = zelda3_version::US;
914 }
915 }
916
917 if (options.populate_palettes) {
918#ifdef __EMSCRIPTEN__
919 EM_ASM({ console.log('[C++] LoadZelda3 - Loading palettes...'); });
920#endif
923#ifdef __EMSCRIPTEN__
924 EM_ASM({ console.log('[C++] LoadZelda3 - Palettes loaded successfully'); });
925#endif
926 } else {
928 }
929
930 if (options.populate_gfx_groups) {
931#ifdef __EMSCRIPTEN__
932 EM_ASM({ console.log('[C++] LoadZelda3 - Loading gfx groups...'); });
933#endif
935#ifdef __EMSCRIPTEN__
936 EM_ASM({ console.log('[C++] LoadZelda3 - Gfx groups loaded successfully'); });
937#endif
938 } else {
941 spriteset_ids = {};
942 paletteset_ids = {};
943 }
944
945 if (options.expand_to_full_image) {
946 if (rom_data_.size() < kBaseRomSize * 2) {
947 rom_data_.resize(kBaseRomSize * 2);
948 }
949 }
950
951 size_ = rom_data_.size();
952
953 return absl::OkStatus();
954}
955
956absl::Status Rom::LoadGfxGroups() {
957 if (rom_data_.empty()) {
958 return absl::FailedPreconditionError("ROM data is empty");
959 }
960
961 // Check if version is in the constants map (this also validates the version)
962 if (kVersionConstantsMap.find(version_) == kVersionConstantsMap.end()) {
963 return absl::FailedPreconditionError(
964 absl::StrFormat("Unsupported ROM version: %d", static_cast<int>(version_)));
965 }
966
967 // Validate kGfxGroupsPointer is within bounds before reading
968 if (kGfxGroupsPointer + 1 >= rom_data_.size()) {
969 LOG_WARN("Rom", "Graphics groups pointer address out of bounds: %u >= %zu",
970 kGfxGroupsPointer + 1, rom_data_.size());
971 return absl::OkStatus(); // Continue with empty groups
972 }
973
975 if (!ptr_status.ok()) return absl::OkStatus();
976
978
979 // Validate converted pointer is within bounds
980 if (main_blockset_ptr >= rom_data_.size()) {
981 LOG_WARN("Rom", "Main blockset pointer out of bounds: %u >= %zu",
983 return absl::OkStatus();
984 }
985
986 // Bounds check for main blocksets
988 if (main_blockset_end > rom_data_.size()) {
989 LOG_WARN("Rom", "Main blockset data out of bounds: %u > %zu",
991 return absl::OkStatus();
992 }
993
994 for (uint32_t i = 0; i < kNumMainBlocksets; i++) {
995 for (int j = 0; j < 8; j++) {
996 uint32_t idx = main_blockset_ptr + (i * 8) + j;
997 // We already checked end bounds, but double check
998 if (idx < rom_data_.size()) {
999 main_blockset_ids[i][j] = rom_data_[idx];
1000 }
1001 }
1002 // DEBUG: Log first blockset to verify
1003 if (i == 0) {
1004 printf("[LoadGfxGroups] Blockset 0: %d %d %d %d %d %d %d %d\n",
1009 }
1010 }
1011
1012 // Bounds check for room blocksets
1013 if (kEntranceGfxGroup >= rom_data_.size()) {
1014 LOG_WARN("Rom", "Entrance graphics group pointer out of bounds: %u >= %zu",
1016 } else {
1017 // Check for integer overflow before calculating end
1020 LOG_WARN("Rom", "Room blockset pointer would overflow: %u + %u",
1022 } else {
1024 if (room_blockset_end <= rom_data_.size()) {
1025 for (uint32_t i = 0; i < kNumRoomBlocksets; i++) {
1026 for (int j = 0; j < 4; j++) {
1027 uint32_t idx = kEntranceGfxGroup + (i * 4) + j;
1028 // Per-access bounds check for WASM safety
1029 if (idx < rom_data_.size()) {
1030 room_blockset_ids[i][j] = rom_data_[idx];
1031 }
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 auto vc = version_constants();
1039
1040 // Bounds check for sprite blocksets
1041 if (vc.kSpriteBlocksetPointer < rom_data_.size()) {
1042 // Check for integer overflow before calculating end
1044 if (vc.kSpriteBlocksetPointer > UINT32_MAX - sprite_blockset_size) {
1045 LOG_WARN("Rom", "Sprite blockset pointer would overflow: %u + %u",
1046 vc.kSpriteBlocksetPointer, sprite_blockset_size);
1047 } else {
1048 uint32_t sprite_blockset_end = vc.kSpriteBlocksetPointer + sprite_blockset_size;
1049 if (sprite_blockset_end <= rom_data_.size()) {
1050 for (uint32_t i = 0; i < kNumSpritesets; i++) {
1051 for (int j = 0; j < 4; j++) {
1052 uint32_t idx = vc.kSpriteBlocksetPointer + (i * 4) + j;
1053 // Per-access bounds check for WASM safety
1054 if (idx < rom_data_.size()) {
1055 spriteset_ids[i][j] = rom_data_[idx];
1056 }
1057 }
1058 }
1059 }
1060 }
1061 }
1062
1063 // Bounds check for palette sets
1064 if (vc.kDungeonPalettesGroups < rom_data_.size()) {
1065 // Check for integer overflow before calculating end
1067 if (vc.kDungeonPalettesGroups > UINT32_MAX - palette_size) {
1068 LOG_WARN("Rom", "Palette groups pointer would overflow: %u + %u",
1069 vc.kDungeonPalettesGroups, palette_size);
1070 } else {
1071 uint32_t palette_end = vc.kDungeonPalettesGroups + palette_size;
1072 if (palette_end <= rom_data_.size()) {
1073 for (uint32_t i = 0; i < kNumPalettesets; i++) {
1074 for (int j = 0; j < 4; j++) {
1075 uint32_t idx = vc.kDungeonPalettesGroups + (i * 4) + j;
1076 // Per-access bounds check for WASM safety
1077 if (idx < rom_data_.size()) {
1078 paletteset_ids[i][j] = rom_data_[idx];
1079 }
1080 }
1081 }
1082 }
1083 }
1084 }
1085
1086 return absl::OkStatus();
1087}
1088
1089absl::Status Rom::SaveGfxGroups() {
1092
1093 for (uint32_t i = 0; i < kNumMainBlocksets; i++) {
1094 for (int j = 0; j < 8; j++) {
1095 rom_data_[main_blockset_ptr + (i * 8) + j] = main_blockset_ids[i][j];
1096 }
1097 }
1098
1099 for (uint32_t i = 0; i < kNumRoomBlocksets; i++) {
1100 for (int j = 0; j < 4; j++) {
1101 rom_data_[kEntranceGfxGroup + (i * 4) + j] = room_blockset_ids[i][j];
1102 }
1103 }
1104
1105 for (uint32_t i = 0; i < kNumSpritesets; i++) {
1106 for (int j = 0; j < 4; j++) {
1108 spriteset_ids[i][j];
1109 }
1110 }
1111
1112 for (uint32_t i = 0; i < kNumPalettesets; i++) {
1113 for (int j = 0; j < 4; j++) {
1115 paletteset_ids[i][j];
1116 }
1117 }
1118
1119 return absl::OkStatus();
1120}
1121
1122absl::Status Rom::SaveToFile(const SaveSettings& settings) {
1123 absl::Status non_firing_status;
1124 if (rom_data_.empty()) {
1125 return absl::InternalError("ROM data is empty.");
1126 }
1127
1128 std::string filename = settings.filename;
1129 auto backup = settings.backup;
1130 auto save_new = settings.save_new;
1131
1132 // Check if filename is empty
1133 if (filename == "") {
1135 }
1136
1137 // Check if backup is enabled
1138 if (backup) {
1139 // Create a backup file with timestamp in its name
1140 auto now = std::chrono::system_clock::now();
1141 auto now_c = std::chrono::system_clock::to_time_t(now);
1142 std::string backup_filename =
1143 absl::StrCat(filename, "_backup_", std::ctime(&now_c));
1144
1145 // Remove newline character from ctime()
1146 backup_filename.erase(
1147 std::remove(backup_filename.begin(), backup_filename.end(), '\n'),
1148 backup_filename.end());
1149
1150 // Replace spaces with underscores
1151 std::replace(backup_filename.begin(), backup_filename.end(), ' ', '_');
1152
1153 // Now, copy the original file to the backup file
1154 try {
1155 std::filesystem::copy(filename_, backup_filename,
1156 std::filesystem::copy_options::overwrite_existing);
1157 } catch (const std::filesystem::filesystem_error& e) {
1158 non_firing_status = absl::InternalError(absl::StrCat(
1159 "Could not create backup file: ", backup_filename, " - ", e.what()));
1160 }
1161 }
1162
1163 // Run the other save functions
1164 if (settings.z3_save) {
1165 if (core::FeatureFlags::get().kSaveAllPalettes)
1167 if (core::FeatureFlags::get().kSaveGfxGroups)
1169 }
1170
1171 if (save_new) {
1172 // Create a file of the same name and append the date between the filename
1173 // and file extension
1174 auto now = std::chrono::system_clock::now();
1175 auto now_c = std::chrono::system_clock::to_time_t(now);
1176 auto filename_no_ext = filename.substr(0, filename.find_last_of("."));
1177 std::cout << filename_no_ext << std::endl;
1178 filename = absl::StrCat(filename_no_ext, "_", std::ctime(&now_c));
1179 // Remove spaces from new_filename and replace with _
1180 filename.erase(std::remove(filename.begin(), filename.end(), ' '),
1181 filename.end());
1182 // Remove newline character from ctime()
1183 filename.erase(std::remove(filename.begin(), filename.end(), '\n'),
1184 filename.end());
1185 // Add the file extension back to the new_filename
1186 filename = filename + ".sfc";
1187 std::cout << filename << std::endl;
1188 }
1189
1190 // Open the file for writing and truncate existing content
1191 std::ofstream file(filename.data(), std::ios::binary | std::ios::trunc);
1192 if (!file) {
1193 return absl::InternalError(
1194 absl::StrCat("Could not open ROM file for writing: ", filename));
1195 }
1196
1197 // Save the data to the file
1198 try {
1199 file.write(
1200 static_cast<const char*>(static_cast<const void*>(rom_data_.data())),
1201 rom_data_.size());
1202 } catch (const std::ofstream::failure& e) {
1203 return absl::InternalError(absl::StrCat(
1204 "Error while writing to ROM file: ", filename, " - ", e.what()));
1205 }
1206
1207 // Check for write errors
1208 if (!file) {
1209 return absl::InternalError(
1210 absl::StrCat("Error while writing to ROM file: ", filename));
1211 }
1212
1213 if (non_firing_status.ok())
1214 dirty_ = false;
1215 return non_firing_status.ok() ? absl::OkStatus() : non_firing_status;
1216}
1217
1218absl::Status Rom::SavePalette(int index, const std::string& group_name,
1219 gfx::SnesPalette& palette) {
1220 for (size_t j = 0; j < palette.size(); ++j) {
1221 gfx::SnesColor color = palette[j];
1222 // If the color is modified, save the color to the ROM
1223 if (color.is_modified()) {
1225 WriteColor(gfx::GetPaletteAddress(group_name, index, j), color));
1226 color.set_modified(false); // Reset the modified flag after saving
1227 }
1228 }
1229 return absl::OkStatus();
1230}
1231
1232absl::Status Rom::SaveAllPalettes() {
1234 palette_groups_.for_each([&](gfx::PaletteGroup& group) -> absl::Status {
1235 for (size_t i = 0; i < group.size(); ++i) {
1236 RETURN_IF_ERROR(
1237 SavePalette(i, group.name(), *group.mutable_palette(i)));
1238 }
1239 return absl::OkStatus();
1240 }));
1241
1242 return absl::OkStatus();
1243}
1244
1245absl::StatusOr<uint8_t> Rom::ReadByte(int offset) {
1246 if (offset < 0) {
1247 return absl::FailedPreconditionError("Offset cannot be negative");
1248 }
1249 if (offset >= static_cast<int>(rom_data_.size())) {
1250 return absl::FailedPreconditionError("Offset out of range");
1251 }
1252 return rom_data_[offset];
1253}
1254
1255absl::StatusOr<uint16_t> Rom::ReadWord(int offset) {
1256 if (offset < 0) {
1257 return absl::FailedPreconditionError("Offset cannot be negative");
1258 }
1259 if (offset + 1 >= static_cast<int>(rom_data_.size())) {
1260 return absl::FailedPreconditionError("Offset out of range");
1261 }
1262 auto result = (uint16_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8));
1263 return result;
1264}
1265
1266absl::StatusOr<uint32_t> Rom::ReadLong(int offset) {
1267 if (offset < 0) {
1268 return absl::OutOfRangeError("Offset cannot be negative");
1269 }
1270 if (offset + 2 >= static_cast<int>(rom_data_.size())) {
1271 return absl::OutOfRangeError("Offset out of range");
1272 }
1273 auto result = (uint32_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8) |
1274 (rom_data_[offset + 2] << 16));
1275 return result;
1276}
1277
1278absl::StatusOr<std::vector<uint8_t>> Rom::ReadByteVector(
1279 uint32_t offset, uint32_t length) const {
1280 if (offset + length > static_cast<uint32_t>(rom_data_.size())) {
1281 return absl::OutOfRangeError("Offset and length out of range");
1282 }
1283 std::vector<uint8_t> result;
1284 for (uint32_t i = offset; i < offset + length; i++) {
1285 result.push_back(rom_data_[i]);
1286 }
1287 return result;
1288}
1289
1290absl::StatusOr<gfx::Tile16> Rom::ReadTile16(uint32_t tile16_id) {
1291 // Skip 8 bytes per tile.
1292 auto tpos = kTile16Ptr + (tile16_id * 0x08);
1293 gfx::Tile16 tile16 = {};
1294 ASSIGN_OR_RETURN(auto new_tile0, ReadWord(tpos));
1295 tile16.tile0_ = gfx::WordToTileInfo(new_tile0);
1296 tpos += 2;
1297 ASSIGN_OR_RETURN(auto new_tile1, ReadWord(tpos));
1298 tile16.tile1_ = gfx::WordToTileInfo(new_tile1);
1299 tpos += 2;
1300 ASSIGN_OR_RETURN(auto new_tile2, ReadWord(tpos));
1301 tile16.tile2_ = gfx::WordToTileInfo(new_tile2);
1302 tpos += 2;
1303 ASSIGN_OR_RETURN(auto new_tile3, ReadWord(tpos));
1304 tile16.tile3_ = gfx::WordToTileInfo(new_tile3);
1305 return tile16;
1306}
1307
1308absl::Status Rom::WriteTile16(int tile16_id, const gfx::Tile16& tile) {
1309 // Skip 8 bytes per tile.
1310 auto tpos = kTile16Ptr + (tile16_id * 0x08);
1311 RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile0_)));
1312 tpos += 2;
1313 RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile1_)));
1314 tpos += 2;
1315 RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile2_)));
1316 tpos += 2;
1317 RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile3_)));
1318 return absl::OkStatus();
1319}
1320
1321absl::Status Rom::WriteByte(int addr, uint8_t value) {
1322 if (addr >= static_cast<int>(rom_data_.size())) {
1323 return absl::OutOfRangeError(absl::StrFormat(
1324 "Attempt to write byte %#02x value failed, address %d out of range",
1325 value, addr));
1326 }
1327 const uint8_t old_val = rom_data_[addr];
1328 rom_data_[addr] = value;
1329 LOG_DEBUG("Rom", "WriteByte: %#06X: %s", addr, util::HexByte(value).data());
1330 dirty_ = true;
1331#ifdef __EMSCRIPTEN__
1332 MaybeBroadcastChange(addr, {old_val}, {value});
1333#endif
1334 return absl::OkStatus();
1335}
1336
1337absl::Status Rom::WriteWord(int addr, uint16_t value) {
1338 if (addr + 1 >= static_cast<int>(rom_data_.size())) {
1339 return absl::OutOfRangeError(absl::StrFormat(
1340 "Attempt to write word %#04x value failed, address %d out of range",
1341 value, addr));
1342 }
1343 const uint8_t old0 = rom_data_[addr];
1344 const uint8_t old1 = rom_data_[addr + 1];
1345 rom_data_[addr] = (uint8_t)(value & 0xFF);
1346 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
1347 LOG_DEBUG("Rom", "WriteWord: %#06X: %s", addr, util::HexWord(value).data());
1348 dirty_ = true;
1349#ifdef __EMSCRIPTEN__
1350 MaybeBroadcastChange(addr, {old0, old1},
1351 {static_cast<uint8_t>(value & 0xFF),
1352 static_cast<uint8_t>((value >> 8) & 0xFF)});
1353#endif
1354 return absl::OkStatus();
1355}
1356
1357absl::Status Rom::WriteShort(int addr, uint16_t value) {
1358 if (addr + 1 >= static_cast<int>(rom_data_.size())) {
1359 return absl::OutOfRangeError(absl::StrFormat(
1360 "Attempt to write short %#04x value failed, address %d out of range",
1361 value, addr));
1362 }
1363 const uint8_t old0 = rom_data_[addr];
1364 const uint8_t old1 = rom_data_[addr + 1];
1365 rom_data_[addr] = (uint8_t)(value & 0xFF);
1366 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
1367 LOG_DEBUG("Rom", "WriteShort: %#06X: %s", addr, util::HexWord(value).data());
1368 dirty_ = true;
1369#ifdef __EMSCRIPTEN__
1370 MaybeBroadcastChange(addr, {old0, old1},
1371 {static_cast<uint8_t>(value & 0xFF),
1372 static_cast<uint8_t>((value >> 8) & 0xFF)});
1373#endif
1374 return absl::OkStatus();
1375}
1376
1377absl::Status Rom::WriteLong(uint32_t addr, uint32_t value) {
1378 if (addr + 2 >= static_cast<uint32_t>(rom_data_.size())) {
1379 return absl::OutOfRangeError(absl::StrFormat(
1380 "Attempt to write long %#06x value failed, address %d out of range",
1381 value, addr));
1382 }
1383 const uint8_t old0 = rom_data_[addr];
1384 const uint8_t old1 = rom_data_[addr + 1];
1385 const uint8_t old2 = rom_data_[addr + 2];
1386 rom_data_[addr] = (uint8_t)(value & 0xFF);
1387 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
1388 rom_data_[addr + 2] = (uint8_t)((value >> 16) & 0xFF);
1389 LOG_DEBUG("Rom", "WriteLong: %#06X: %s", addr, util::HexLong(value).data());
1390 dirty_ = true;
1391#ifdef __EMSCRIPTEN__
1392 MaybeBroadcastChange(addr, {old0, old1, old2},
1393 {static_cast<uint8_t>(value & 0xFF),
1394 static_cast<uint8_t>((value >> 8) & 0xFF),
1395 static_cast<uint8_t>((value >> 16) & 0xFF)});
1396#endif
1397 return absl::OkStatus();
1398}
1399
1400absl::Status Rom::WriteVector(int addr, std::vector<uint8_t> data) {
1401 if (addr + static_cast<int>(data.size()) >
1402 static_cast<int>(rom_data_.size())) {
1403 return absl::InvalidArgumentError(absl::StrFormat(
1404 "Attempt to write vector value failed, address %d out of range", addr));
1405 }
1406 std::vector<uint8_t> old_data;
1407 old_data.reserve(data.size());
1408 for (int i = 0; i < static_cast<int>(data.size()); i++) {
1409 old_data.push_back(rom_data_[addr + i]);
1410 }
1411 for (int i = 0; i < static_cast<int>(data.size()); i++) {
1412 rom_data_[addr + i] = data[i];
1413 }
1414 LOG_DEBUG("Rom", "WriteVector: %#06X: %s", addr,
1415 util::HexByte(data[0]).data());
1416 dirty_ = true;
1417#ifdef __EMSCRIPTEN__
1418 MaybeBroadcastChange(addr, old_data, data);
1419#endif
1420 return absl::OkStatus();
1421}
1422
1423absl::Status Rom::WriteColor(uint32_t address, const gfx::SnesColor& color) {
1424 uint16_t bgr = ((color.snes() >> 10) & 0x1F) | ((color.snes() & 0x1F) << 10) |
1425 (color.snes() & 0x7C00);
1426
1427 // Write the 16-bit color value to the ROM at the specified address
1428 LOG_DEBUG("Rom", "WriteColor: %#06X: %s", address, util::HexWord(bgr).data());
1429 auto st = WriteShort(address, bgr);
1430 if (st.ok())
1431 dirty_ = true;
1432 return st;
1433}
1434
1435} // 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:24
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:243
zelda3_version version_
Definition rom_old.h:282
auto mutable_graphics_buffer()
Definition rom_old.h:230
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:74
auto begin()
Definition rom.h:137
gfx::PaletteGroupMap palette_groups_
Definition rom_old.h:279
absl::Status WriteColor(uint32_t address, const gfx::SnesColor &color)
Definition rom.cc:357
auto filename() const
Definition rom.h:141
absl::Status LoadZelda3()
Definition rom_old.cc:851
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition rom_old.h:247
const auto & vector() const
Definition rom.h:139
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition rom_old.h:248
absl::StatusOr< uint16_t > ReadWord(int offset)
Definition rom.cc:228
const auto & palette_group() const
Definition rom_old.h:231
std::string title_
Definition rom.h:155
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:164
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition rom_old.h:246
GraphicsLoadDiagnostics & GetMutableDiagnostics()
Definition rom_old.h:251
std::vector< uint8_t > rom_data_
Definition rom.h:164
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
zelda3_version_pointers version_constants() const
Definition rom_old.h:241
absl::Status SaveGfxGroups()
Definition rom_old.cc:1089
absl::Status LoadGfxGroups()
Definition rom_old.cc:956
bool dirty_
Definition rom.h:170
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:147
std::string filename_
Definition rom.h:158
unsigned long size_
Definition rom.h:152
absl::Status SavePalette(int index, const std::string &group_name, gfx::SnesPalette &palette)
Definition rom_old.cc:1218
project::ResourceLabelManager resource_label_manager_
Definition rom.h:167
std::string short_name_
Definition rom.h:161
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition rom_old.h:245
absl::Status SaveAllPalettes()
Definition rom_old.cc:1232
static Flags & get()
Definition features.h:92
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:199
SNES Color container.
Definition snes_color.h:110
constexpr void set_modified(bool m)
Definition snes_color.h:198
constexpr bool is_modified() const
Definition snes_color.h:195
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
Tile composition of four 8x8 tiles.
Definition snes_tile.h:140
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< std::vector< uint8_t > > DecompressV2(const uint8_t *data, int offset, int size, int mode, size_t rom_size)
Decompresses a buffer of data using the LC_LZ2 algorithm.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
constexpr int kTilesheetDepth
Definition snes_tile.h:18
std::vector< uint8_t > Bpp8SnesToIndexed(std::vector< uint8_t > data, uint64_t bpp)
Definition snes_tile.cc:202
std::vector< uint8_t > SnesTo8bppSheet(std::span< uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:131
uint32_t GetPaletteAddress(const std::string &group_name, size_t palette_index, size_t color_index)
std::vector< uint8_t > HyruleMagicCompress(uint8_t const *const src, int const oldsize, int *const size, int const flag)
absl::Status LoadAllPalettes(const std::vector< uint8_t > &rom_data, PaletteGroupMap &groups)
Loads all the palettes for the game.
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
constexpr uint32_t kGfxGroupsPointer
Definition rom_old.h:50
absl::StatusOr< std::vector< uint8_t > > Load2BppGraphics(const Rom &rom)
Loads 2bpp graphics from Rom data.
Definition rom_old.cc:206
absl::StatusOr< std::array< gfx::Bitmap, kNumLinkSheets > > LoadLinkGraphics(const Rom &rom)
Loads the players 4bpp graphics sheet from Rom data.
Definition rom_old.cc:235
uint32_t GetGraphicsAddress(const uint8_t *data, uint8_t addr, uint32_t ptr1, uint32_t ptr2, uint32_t ptr3, size_t rom_size)
Calculates ROM offset for a graphics sheet using pointer tables.
Definition rom_old.cc:175
constexpr int Uncompressed3BPPSize
Definition rom_old.cc:51
constexpr uint32_t kNumLinkSheets
Definition rom_old.h:33
constexpr uint32_t kEntranceGfxGroup
Definition rom_old.h:56
constexpr uint32_t kNumSpritesets
Definition rom_old.h:54
int AddressFromBytes(uint8_t bank, uint8_t high, uint8_t low) noexcept
Definition snes.h:39
constexpr uint32_t kTile16Ptr
Definition rom_old.h:34
constexpr uint32_t kNumMainBlocksets
Definition rom_old.h:52
constexpr uint32_t kNumRoomBlocksets
Definition rom_old.h:53
absl::StatusOr< gfx::Bitmap > LoadFontGraphics(const Rom &rom)
Definition rom_old.cc:256
constexpr uint32_t kNumGfxSheets
Definition rom_old.h:32
absl::StatusOr< std::array< gfx::Bitmap, kNumGfxSheets > > LoadAllGraphicsData(Rom &rom, bool defer_render)
This function iterates over all graphics sheets in the Rom and loads them into memory....
Definition rom_old.cc:352
constexpr uint32_t kNumPalettesets
Definition rom_old.h:55
absl::Status SaveAllGraphicsData(Rom &rom, std::array< gfx::Bitmap, kNumGfxSheets > &gfx_sheets)
Definition rom_old.cc:663
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
static RomLoadOptions AppDefaults()
Definition rom_old.cc:119
bool expand_to_full_image
Definition rom_old.h:65
static RomLoadOptions CliDefaults()
Definition rom_old.cc:123
static RomLoadOptions RawDataOnly()
Definition rom_old.cc:132
bool load_resource_labels
Definition rom_old.h:66
absl::Status for_each(Func &&func)
Represents a group of palettes.
bool LoadLabels(const std::string &filename)
Definition project.cc:1204
uint32_t kOverworldGfxPtr1
Definition zelda.h:102
uint32_t kOverworldGfxPtr2
Definition zelda.h:103
uint32_t kSpriteBlocksetPointer
Definition zelda.h:109
uint32_t kOverworldGfxPtr3
Definition zelda.h:104
uint32_t kDungeonPalettesGroups
Definition zelda.h:110
The Legend of Zelda: A Link to the Past - Data Structures and Constants.