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