yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_map.cc
Go to the documentation of this file.
1#include "overworld_map.h"
2
3#include <algorithm>
4#include <array>
5#include <atomic>
6#include <cstddef>
7#include <cstdint>
8#include <unordered_map>
9#include <vector>
10
11#include "absl/status/status.h"
12#include "absl/status/statusor.h"
16#include "core/features.h"
17#include "rom/rom.h"
18#include "util/macro.h"
19#include "zelda3/common.h"
22
23namespace yaze::zelda3 {
24
25namespace {
26
27bool IsDeathMountainArea(int area) {
28 return area == 0x03 || area == 0x05 || area == 0x07 || area == 0x43 ||
29 area == 0x45 || area == 0x47;
30}
31
32// Vanilla subscreen overlay per screen (fog, sky, lava, pyramid, curtains).
33// Matches the vanilla ZSCustomOverworld OverlayTable and ZScream defaults.
34uint16_t VanillaSubscreenOverlay(int index) {
35 switch (index) {
36 case 0x00:
37 case 0x01:
38 case 0x08:
39 case 0x09:
40 case 0x40:
41 case 0x41:
42 case 0x48:
43 case 0x49:
44 return 0x009D; // Fog 2: Lost Woods / Skull Woods
45 case 0x03:
46 case 0x04:
47 case 0x0B:
48 case 0x0C:
49 case 0x05:
50 case 0x06:
51 case 0x0D:
52 case 0x0E:
53 case 0x07:
54 return 0x0095; // Sky background: LW Death Mountain
55 case 0x43:
56 case 0x44:
57 case 0x4B:
58 case 0x4C:
59 case 0x45:
60 case 0x46:
61 case 0x4D:
62 case 0x4E:
63 case 0x47:
64 return 0x009C; // Lava background: DW Death Mountain
65 case 0x5B:
66 case 0x5C:
67 case 0x63:
68 case 0x64:
69 return 0x0096; // Pyramid background
70 case 0x80:
71 return 0x0097; // Fog 1: Master Sword area
72 case 0x88:
73 return 0x0093; // Triforce room curtains
74 default:
75 return 0x00FF;
76 }
77}
78
79} // namespace
80
81OverworldMap::OverworldMap(int index, Rom* rom, GameData* game_data)
82 : OverworldMap(index, rom, game_data, /*seed_area_parent=*/true) {}
83
84OverworldMap::OverworldMap(int index, Rom* rom, GameData* game_data,
85 bool seed_area_parent, bool skip_legacy_data)
86 : index_(index), parent_(index), rom_(rom), game_data_(game_data) {
87 if (skip_legacy_data) {
88 return;
89 }
90 // Load parent ID from ROM data for all versions
91 // This is critical for proper large map sibling coordination
94 // For v3+, parent ID is stored in expanded table (0x140998) with 160 entries
96 } else {
97 // For vanilla/v1/v2, parent ID table at 0x125EC only has 64 entries (LW only)
98 // DW maps mirror LW parents with +0x40 offset
99 // SW maps use hardcoded parent values based on vanilla game behavior
101 // Light World: direct lookup from 64-entry table
103 } else if (index_ < kSpecialWorldMapIdStart) {
104 // Dark World: mirror LW parent structure with +0x40 offset
105 // DW map 0x43's parent = LW map 0x03's parent (from table) + 0x40
106 uint8_t lw_equivalent = index_ - kDarkWorldMapIdStart;
107 uint8_t lw_parent = (*rom_)[kOverworldMapParentId + lw_equivalent];
108 parent_ = lw_parent + kDarkWorldMapIdStart;
109 } else {
110 // Special World: hardcoded parents for vanilla compatibility
111 // Zora's Domain (0x81, 0x82, 0x89, 0x8A) is a large area with parent 0x81
112 if (index_ == 0x81 || index_ == 0x82 || index_ == 0x89 ||
113 index_ == 0x8A) {
114 parent_ = 0x81;
115 } else {
116 // All other SW maps are small areas - parent is self
117 parent_ = index_;
118 }
119 }
120 }
121
122 LoadAreaInfo();
123
124 if (version != OverworldVersion::kVanilla) {
125 // For ALL custom ASM versions (v1-v3), read from custom arrays
126 // SetupCustomTileset checks enable flags and falls back to vanilla if disabled
128 } else if (core::FeatureFlags::get().overworld.kLoadCustomOverworld) {
129 // Pure vanilla ROM but flag enabled - set up hardcoded vanilla defaults
131 } else {
132 // Pure vanilla ROM: the subscreen overlays are hardcoded in the game.
133 subscreen_overlay_ = VanillaSubscreenOverlay(index_);
134 }
135
136 // Child screens of a multi-screen area render with the parent's area
137 // settings (the game reads the tables with $8A = parent). Seed them from
138 // the parent's ROM entries; Overworld re-syncs from the live parent map
139 // before builds so unsaved parent edits reach the children.
140 if (seed_area_parent && parent_ != index_ && parent_ >= 0 &&
142 OverworldMap area_parent(parent_, rom_, game_data_,
143 /*seed_area_parent=*/false);
144 InheritAreaProperties(area_parent);
145 }
147}
148
149absl::StatusOr<OverworldMap> OverworldMap::CreateLyraPreview(
150 int index, Rom* rom, GameData* game_data,
151 const rom::LyraLayoutInspection* layout) {
152 if (rom == nullptr || layout == nullptr ||
154 index >= layout->capacity) {
155 return absl::InvalidArgumentError("Invalid Lyra preview map");
156 }
157 OverworldMap map(index, rom, game_data, /*seed_area_parent=*/false,
158 /*skip_legacy_data=*/true);
159 map.descriptor_layout_ = layout;
161 return map;
162}
163
165 auto read = [this](uint16_t id,
166 size_t index) -> absl::StatusOr<std::span<const uint8_t>> {
168 rom_->size(), id, index);
169 if (!record) {
170 return absl::FailedPreconditionError("Lyra preview property unavailable");
171 }
172 return *record;
173 };
174 ASSIGN_OR_RETURN(auto parent, read(7, index_));
175 parent_ = parent[0];
177 return absl::FailedPreconditionError("Lyra preview parent out of range");
178 }
179 ASSIGN_OR_RETURN(auto size, read(30, index_));
180 if (size[0] > 3) {
181 return absl::FailedPreconditionError("Lyra preview area size invalid");
182 }
183 area_size_ = static_cast<AreaSizeEnum>(size[0]);
185 ASSIGN_OR_RETURN(auto bg, read(1, parent_));
186 area_specific_bg_color_ = static_cast<uint16_t>(bg[0] | (bg[1] << 8));
187 ASSIGN_OR_RETURN(auto palette, read(2, parent_));
188 main_palette_ = palette[0];
189 ASSIGN_OR_RETURN(auto mosaic, read(3, parent_));
190 mosaic_ = mosaic[0] != 0;
191 for (int i = 0; i < 4; ++i) {
192 mosaic_expanded_[i] = (mosaic[0] & (0x08 >> i)) != 0;
193 }
194 ASSIGN_OR_RETURN(auto animated, read(4, parent_));
195 animated_gfx_ = animated[0];
196 ASSIGN_OR_RETURN(auto overlay, read(5, parent_));
197 subscreen_overlay_ = static_cast<uint16_t>(overlay[0] | (overlay[1] << 8));
198 ASSIGN_OR_RETURN(auto gfx, read(6, parent_));
199 std::copy_n(gfx.begin(), custom_gfx_ids_.size(), custom_gfx_ids_.begin());
200 ASSIGN_OR_RETURN(auto area_gfx, read(26, parent_));
201 area_graphics_ = area_gfx[0];
202 ASSIGN_OR_RETURN(auto palette_set, read(31, parent_));
203 area_palette_ = palette_set[0];
205 const size_t special = parent_ - kSpecialWorldMapIdStart;
206 ASSIGN_OR_RETURN(auto sprite_gfx, read(24, special));
207 ASSIGN_OR_RETURN(auto sprite_palette, read(25, special));
208 sprite_graphics_.fill(sprite_gfx[0]);
210 }
211 return absl::OkStatus();
212}
213
226
228 if (&area_parent == this || area_parent.index_ == index_) {
229 inherited_area_.reset();
230 return;
231 }
233 props.main_palette = area_parent.main_palette_;
234 props.animated_gfx = area_parent.animated_gfx_;
235 props.custom_gfx_ids = area_parent.custom_gfx_ids_;
236 props.subscreen_overlay = area_parent.subscreen_overlay_;
238 inherited_area_ = props;
239}
240
243 for (int i = 0; i < 16; ++i) {
244 key[i] = static_graphics_[i];
245 }
246 key[16] = animated_sheet_;
247 return key;
248}
249
250absl::Status OverworldMap::BuildMap(int count, int game_state, int world,
251 std::vector<gfx::Tile16>& tiles16,
252 OverworldBlockset& world_blockset) {
253 // Delegate to cached version with no cache
254 return BuildMapWithCache(count, game_state, world, tiles16, world_blockset,
255 nullptr);
256}
257
259 int count, int game_state, int world, std::vector<gfx::Tile16>& tiles16,
260 OverworldBlockset& world_blockset,
261 const std::vector<uint8_t>* cached_tileset,
262 const std::vector<uint8_t>* cached_tile16_blockset) {
264 world_ = world;
266
267 // For large maps in vanilla ROMs, we need to handle special world graphics
268 // This ensures proper rendering of special overworld areas like Zora's Domain
269 // NOTE: Callers (LoadOverworldMaps, EnsureMapBuilt) also handle this and call
270 // LoadAreaGraphics() before BuildMapWithCache. This block is kept for the
271 // native async path which calls BuildMap() -> BuildMapWithCache() directly.
272 if (large_map_ && (version == OverworldVersion::kVanilla)) {
273 if (parent_ != index_ && !initialized_) {
274 if (index_ >= kSpecialWorldMapIdStart && index_ <= 0x8A &&
275 index_ != 0x88) {
276 // Zora's Domain children - set sprite_graphics and area graphics
277 sprite_graphics_[0] = 0x0E;
278 sprite_graphics_[1] = 0x0E;
279 sprite_graphics_[2] = 0x0E;
283 } else if (index_ == 0x88) {
284 // Triforce room has special hardcoded values
285 area_graphics_ = 0x51;
286 area_palette_ = 0x00;
287 } else if (index_ < kSpecialWorldMapIdStart) {
288 // LW/DW large map child - use parent's graphics
291 }
292 // Note: Other SW maps (>0x8A) keep their LoadAreaInfo values
293
294 initialized_ = true;
295 }
296 }
297
299
300 // Use cached tileset if available, otherwise build from scratch
301 if (cached_tileset && !cached_tileset->empty()) {
302 UseCachedTileset(*cached_tileset);
303 } else {
305 }
306
307 // The tile16 blockset only depends on the tileset and the tile16
308 // definitions, so a shared copy can be reused (see Overworld's tileset
309 // cache).
310 if (!HasGraphicsPreview() && cached_tileset && !cached_tileset->empty() &&
311 cached_tile16_blockset && !cached_tile16_blockset->empty()) {
312 current_blockset_ = *cached_tile16_blockset;
313 } else {
314 RETURN_IF_ERROR(BuildTiles16Gfx(tiles16, count))
315 }
318 RETURN_IF_ERROR(BuildBitmap(world_blockset))
319 built_ = true;
320 return absl::OkStatus();
321}
322
325
326 // ZSCustomOverworld ASM Version System:
327 // 0x00-0x02: Legacy versions with limited features
328 // 0x03: Current version with full area size expansion and custom data support
329 // 0xFF: Pure vanilla ROM (no ASM applied)
330
331 // Load message ID and area size based on ASM version. v1+ moves message
332 // ids into expanded space, but only v3 supports per-map area-size enums.
334 // Keep persistent per-screen metadata separate from parent-derived display
335 // state. Unused child slots can contain distinct authored values.
337 (*rom_)[GetOverworldMessagesExpanded() + (index_ * 2)] |
338 ((*rom_)[GetOverworldMessagesExpanded() + (index_ * 2) + 1] << 8);
339 } else {
340 message_id_ = (*rom_)[kOverworldMessageIds + (parent_ * 2)] |
341 ((*rom_)[kOverworldMessageIds + (parent_ * 2) + 1] << 8);
342 }
343
344 // Persistent colors must be loaded even when this map is never rendered.
345 // LoadPalette must not replace this value during visual refreshes.
349 ((*rom_)[OverworldCustomAreaSpecificBGPalette + (index_ * 2) + 1] << 8);
350 }
351
353 switch ((*rom_)[kOverworldScreenSize + index_]) {
354 case 1:
356 break;
357 case 2:
359 break;
360 case 3:
362 break;
363 case 0:
364 default:
366 break;
367 }
368 } else {
369 if (index_ < 0x80) {
370 // For LW and DW, check the world-specific screen size byte. The legacy
371 // parent table is shared/local, but screen-size data has separate
372 // Light/Dark entries.
373 uint8_t size_byte = (*rom_)[kOverworldScreenSize +
375 switch (size_byte) {
376 case 0:
378 break;
379 case 1:
380 default:
382 break;
383 }
384 } else {
385 // For SW, use hardcoded values for vanilla/v1/v2 compatibility
386 // Zora's Domain areas (0x81, 0x82, 0x89, 0x8A) are large areas
387 area_size_ =
388 (index_ == 0x81 || index_ == 0x82 || index_ == 0x89 || index_ == 0x8A)
391 }
392 }
393
394 // Update large_map_ based on area size
396
397 // Load area-specific data based on index range
399 // Light World (LW) areas
405
408
410 sprite_palette_[1] =
412 sprite_palette_[2] =
414
419
420 // For v2/vanilla, use original palette table
421 if (version < OverworldVersion::kZSCustomV2 ||
422 version == OverworldVersion::kVanilla) {
424 }
425 } else if (index_ < kSpecialWorldMapIdStart) {
426 // Dark World (DW) areas
433
436
437 sprite_palette_[0] =
439 sprite_palette_[1] =
441 sprite_palette_[2] =
443
444 area_music_[0] =
446
447 // For v2/vanilla, use original palette table
448 if (version < OverworldVersion::kZSCustomV2 ||
449 version == OverworldVersion::kVanilla) {
451 }
452 } else {
453 // Special World (SW) areas (index >= 0x80)
454 // Message ID already loaded above based on ASM version
455
456 // For v3, use expanded sprite tables with full customization support
457 if (version == OverworldVersion::kZSCustomV3) {
467
474 } else {
475 // For v2/vanilla, use original sprite tables
482
489 }
490
493
494 // For v2/vanilla, use original palette table and handle special cases
495 // These hardcoded cases are needed for vanilla compatibility
496 if (version < OverworldVersion::kZSCustomV2 ||
497 version == OverworldVersion::kVanilla) {
499
500 // Handle special world area cases based on ZScream documentation
501 if (index_ == 0x88 || index_ == 0x93) {
502 // Triforce room - special graphics and palette
503 area_graphics_ = 0x51;
504 area_palette_ = 0x00;
505 } else if (index_ == 0x80) {
506 // Master Sword area - use special graphics group
510 } else if (index_ == 0x81 || index_ == 0x82 || index_ == 0x89 ||
511 index_ == 0x8A) {
512 // Zora's Domain areas - use special sprite graphics and area graphics
513 // Note: These are the large area maps that were causing crashes
514 sprite_graphics_[0] = 0x0E;
515 sprite_graphics_[1] = 0x0E;
516 sprite_graphics_[2] = 0x0E;
517
521 } else if (index_ == 0x94) {
522 // Make this the same GFX as the true master sword area
524 (0x80 - kSpecialWorldMapIdStart)];
526 } else if (index_ == 0x95) {
527 // Make this the same GFX as the LW death mountain areas
528 area_graphics_ = (*rom_)[kAreaGfxIdPtr + 0x03];
529 area_palette_ = (*rom_)[kOverworldMapPaletteIds + 0x03];
530 } else if (index_ == 0x96) {
531 // Make this the same GFX as the pyramid areas
532 area_graphics_ = (*rom_)[kAreaGfxIdPtr + 0x5B];
533 area_palette_ = (*rom_)[kOverworldMapPaletteIds + 0x5B];
534 } else if (index_ == 0x9C) {
535 // Make this the same GFX as the DW death mountain areas
536 area_graphics_ = (*rom_)[kAreaGfxIdPtr + 0x43];
537 area_palette_ = (*rom_)[kOverworldMapPaletteIds + 0x43];
538 } else {
539 // Default case - use basic graphics
540 area_graphics_ = (*rom_)[kAreaGfxIdPtr + 0x00];
541 area_palette_ = (*rom_)[kOverworldMapPaletteIds + 0x00];
542 }
543 }
544 }
545}
546
548 const auto version = OverworldVersionHelper::GetVersion(*rom_);
554 const int palette_table =
558 raw_area_info_[0] = (*rom_)[kAreaGfxIdPtr + index_];
559 raw_area_info_[1] = (*rom_)[palette_table + index_];
560 for (int state = 0; state < 3; ++state) {
562 const int slot = index_ < kDarkWorldMapIdStart
563 ? index_ + state * kDarkWorldMapIdStart
565 raw_area_info_[2 + state] = (*rom_)[kOverworldSpriteset + slot];
566 raw_area_info_[5 + state] = (*rom_)[kOverworldSpritePaletteIds + slot];
567 } else {
568 const bool expanded = OverworldVersionHelper::SupportsAreaEnum(version);
569 const int gfx_table = expanded
572 const int palette_table = expanded
575 const int slot = index_ - kSpecialWorldMapIdStart;
576 raw_area_info_[2 + state] = (*rom_)[gfx_table + slot];
577 raw_area_info_[5 + state] = (*rom_)[palette_table + slot];
578 }
579 }
581}
582
584 // Set the main palette values based on ZScream logic
585 // Use parent_ to ensure all sibling maps in a large area share the same palette
586 if (parent_ < 0x40 || parent_ == 0x95) { // LW
587 main_palette_ = 0;
588 } else if ((parent_ >= 0x40 && parent_ < 0x80) || parent_ == 0x96) { // DW
589 main_palette_ = 1;
590 } else if (parent_ >= 0x80 && parent_ < 0xA0) { // SW
591 main_palette_ = 0;
592 }
593
594 // Death Mountain / special overrides - use parent_ so siblings get correct palette
595 if (parent_ == 0x03 || parent_ == 0x05 ||
596 parent_ == 0x07) { // LW Death Mountain
597 main_palette_ = 2;
598 } else if (parent_ == 0x43 || parent_ == 0x45 ||
599 parent_ == 0x47) { // DW Death Mountain
600 main_palette_ = 3;
601 } else if (parent_ == 0x88 || parent_ == 0x93) { // Triforce room
602 main_palette_ = 4;
603 }
604
605 // Set the mosaic values based on ZScream logic
606 switch (index_) {
607 case 0x00: // Leaving Skull Woods / Lost Woods
608 case 0x40:
609 mosaic_expanded_ = {false, true, false, true};
610 break;
611 case 0x02: // Going into Skull woods / Lost Woods west
612 case 0x0A:
613 case 0x42:
614 case 0x4A:
615 mosaic_expanded_ = {false, false, true, false};
616 break;
617 case 0x0F: // Going into Zora's Domain North
618 case 0x10: // Going into Skull Woods / Lost Woods North
619 case 0x11:
620 case 0x50:
621 case 0x51:
622 mosaic_expanded_ = {true, false, false, false};
623 break;
624 case 0x80: // Leaving Zora's Domain, the Master Sword area, and the
625 // Triforce area
626 case 0x81:
627 case 0x88:
628 mosaic_expanded_ = {false, true, false, false};
629 break;
630 default:
631 mosaic_expanded_ = {false, false, false, false};
632 break;
633 }
634
635 // Set up world index for GFX groups
636 int index_world = 0x20;
639 index_world = 0x21;
640 } else if (parent_ == 0x88 || parent_ == 0x93) { // Triforce room
641 index_world = 0x24;
642 }
643
644 const auto overworld_gfx_groups2 = version_constants().kOverworldGfxGroups2;
645
646 // Main Blocksets
647 for (int i = 0; i < 8; i++) {
648 custom_gfx_ids_[i] =
649 (uint8_t)(*rom_)[overworld_gfx_groups2 + (index_world * 8) + i];
650 }
651
652 const auto overworldgfxGroups = version_constants().kOverworldGfxGroups1;
653
654 // Replace the variable tiles with the variable ones
655 uint8_t temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4)];
656 if (temp != 0) {
657 custom_gfx_ids_[3] = temp;
658 } else {
659 custom_gfx_ids_[3] = 0xFF;
660 }
661
662 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 1];
663 if (temp != 0) {
664 custom_gfx_ids_[4] = temp;
665 } else {
666 custom_gfx_ids_[4] = 0xFF;
667 }
668
669 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 2];
670 if (temp != 0) {
671 custom_gfx_ids_[5] = temp;
672 } else {
673 custom_gfx_ids_[5] = 0xFF;
674 }
675
676 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 3];
677 if (temp != 0) {
678 custom_gfx_ids_[6] = temp;
679 } else {
680 custom_gfx_ids_[6] = 0xFF;
681 }
682
683 // Set the animated GFX values - use parent_ so siblings get correct animated sheet
684 // Death Mountain uses sheet 0x59, others use 0x5B
685 if (parent_ == 0x03 || parent_ == 0x05 || parent_ == 0x07 ||
686 parent_ == 0x43 || parent_ == 0x45 || parent_ == 0x47 ||
687 parent_ == 0x95) {
688 animated_gfx_ = 0x59;
689 } else {
690 animated_gfx_ = 0x5B;
691 }
692
693 // Set the subscreen overlay values
694 subscreen_overlay_ = 0x00FF;
695
696 if (index_ == 0x00 || index_ == 0x01 || index_ == 0x08 || index_ == 0x09 ||
697 index_ == 0x40 || index_ == 0x41 || index_ == 0x48 ||
698 index_ == 0x49) { // Add fog 2 to the lost woods and skull woods
699 subscreen_overlay_ = 0x009D;
700 } else if (index_ == 0x03 || index_ == 0x04 || index_ == 0x0B ||
701 index_ == 0x0C || index_ == 0x05 || index_ == 0x06 ||
702 index_ == 0x0D || index_ == 0x0E ||
703 index_ == 0x07) { // Add the sky BG to LW death mountain
704 subscreen_overlay_ = 0x0095;
705 } else if (index_ == 0x43 || index_ == 0x44 || index_ == 0x4B ||
706 index_ == 0x4C || index_ == 0x45 || index_ == 0x46 ||
707 index_ == 0x4D || index_ == 0x4E ||
708 index_ == 0x47) { // Add the lava to DW death mountain
709 subscreen_overlay_ = 0x009C;
710 } else if (index_ == 0x5B || index_ == 0x5C || index_ == 0x63 ||
711 index_ == 0x64) { // TODO: Might need this one too "index == 0x1B"
712 // but for now I don't think so
713 subscreen_overlay_ = 0x0096;
714 } else if (index_ == 0x80) { // Add fog 1 to the master sword area
715 subscreen_overlay_ = 0x0097;
716 } else if (index_ ==
717 0x88) { // Add the triforce room curtains to the triforce room
718 subscreen_overlay_ = 0x0093;
719 }
720}
721
723 uint8_t palette = 0;
724
725 // Base world selection - use parent_ to ensure siblings share the same palette
726 if (parent_ < 0x40 || parent_ == 0x95) { // LW
727 palette = 0;
728 } else if ((parent_ >= 0x40 && parent_ < 0x80) || parent_ == 0x96) { // DW
729 palette = 1;
730 } else if (parent_ >= 0x80 && parent_ < 0xA0) { // SW
731 palette = 0;
732 }
733
734 // Death Mountain / special overrides - use parent_ to ensure all siblings
735 // in a large area get the correct palette (brown grass for DM)
736 // LW DM parents: 0x03, 0x05, 0x07 (children: 0x04, 0x06, 0x0B-0x0E)
737 // DW DM parents: 0x43, 0x45, 0x47 (children: 0x44, 0x46, 0x4B-0x4E)
738 if (parent_ == 0x03 || parent_ == 0x05 || parent_ == 0x07) {
739 palette = 2; // LW Death Mountain
740 } else if (parent_ == 0x43 || parent_ == 0x45 || parent_ == 0x47) {
741 palette = 3; // DW Death Mountain
742 } else if (parent_ == 0x88 || parent_ == 0x93) {
743 palette = 4; // Triforce room
744 }
745
746 return palette;
747}
748
749void OverworldMap::SetupCustomTileset(uint8_t asm_version) {
750 // Load area size for v3
753 uint8_t size_byte = (*rom_)[kOverworldScreenSize + index_];
754 area_size_ = static_cast<AreaSizeEnum>(size_byte);
756 }
757
758 // Main Palette - check enable flag before reading from custom array
759 if ((*rom_)[OverworldCustomMainPaletteEnabled] != 0x00) {
761 } else {
762 // Fall back to world-based hardcoded logic
764 }
765
766 // Mosaic - check enable flag before reading from custom array
767 if ((*rom_)[OverworldCustomMosaicEnabled] != 0x00) {
768 mosaic_ = (*rom_)[OverworldCustomMosaicArray + index_] != 0x00;
769 uint8_t mosaicByte = (*rom_)[OverworldCustomMosaicArray + index_];
771 (mosaicByte & 0x08) != 0x00, (mosaicByte & 0x04) != 0x00,
772 (mosaicByte & 0x02) != 0x00, (mosaicByte & 0x01) != 0x00};
773 } else {
774 // Fall back to hardcoded vanilla mosaic values
775 mosaic_ = false;
776 mosaic_expanded_ = {false, false, false, false};
777 switch (index_) {
778 case 0x00: // Leaving Skull Woods / Lost Woods
779 case 0x40:
780 mosaic_expanded_ = {false, true, false, true};
781 break;
782 case 0x02: // Going into Skull woods / Lost Woods west
783 case 0x0A:
784 case 0x42:
785 case 0x4A:
786 mosaic_expanded_ = {false, false, true, false};
787 break;
788 case 0x0F: // Going into Zora's Domain North
789 case 0x10: // Going into Skull Woods / Lost Woods North
790 case 0x11:
791 case 0x50:
792 case 0x51:
793 mosaic_expanded_ = {true, false, false, false};
794 break;
795 case 0x80: // Leaving Zora's Domain, Master Sword area, Triforce area
796 case 0x81:
797 case 0x88:
798 mosaic_expanded_ = {false, true, false, false};
799 break;
800 }
801 }
802
803 // Animated GFX - check enable flag before reading from custom array
804 if ((*rom_)[OverworldCustomAnimatedGFXEnabled] != 0x00) {
806 } else {
807 // Death mountain uses 0x59, others use 0x5B
808 // Use parent_ to ensure siblings in large areas share the same animated sheet
809 if (parent_ == 0x03 || parent_ == 0x05 || parent_ == 0x07 ||
810 parent_ == 0x43 || parent_ == 0x45 || parent_ == 0x47 ||
811 parent_ == 0x95) {
812 animated_gfx_ = 0x59;
813 } else {
814 animated_gfx_ = 0x5B;
815 }
816 }
817
818 // Tile GFX Groups - check enable flag before reading from custom array
820 for (int i = 0; i < 8; i++) {
821 custom_gfx_ids_[i] =
822 (*rom_)[OverworldCustomTileGFXGroupArray + (index_ * 8) + i];
823 }
824 } else {
825 // Fall back to world-based GFX groups
826 int index_world = 0x20;
829 index_world = 0x21;
830 } else if (parent_ == 0x88 || parent_ == 0x93) { // Triforce room
831 index_world = 0x24;
832 }
833
834 // Main Blocksets
835 for (int i = 0; i < 8; i++) {
836 custom_gfx_ids_[i] =
838 (index_world * 8) + i];
839 }
840
841 const auto overworldgfxGroups = version_constants().kOverworldGfxGroups1;
842
843 // Replace the variable tiles with the variable ones
844 // If the variable is 00 set it to 0xFF which is the new "don't load
845 // anything" value
846 uint8_t temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4)];
847 if (temp != 0x00) {
848 custom_gfx_ids_[3] = temp;
849 } else {
850 custom_gfx_ids_[3] = 0xFF;
851 }
852
853 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 1];
854 if (temp != 0x00) {
855 custom_gfx_ids_[4] = temp;
856 } else {
857 custom_gfx_ids_[4] = 0xFF;
858 }
859
860 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 2];
861 if (temp != 0x00) {
862 custom_gfx_ids_[5] = temp;
863 } else {
864 custom_gfx_ids_[5] = 0xFF;
865 }
866
867 temp = (*rom_)[overworldgfxGroups + (area_graphics_ * 4) + 3];
868 if (temp != 0x00) {
869 custom_gfx_ids_[6] = temp;
870 } else {
871 custom_gfx_ids_[6] = 0xFF;
872 }
873 }
874
875 // Subscreen Overlay - check enable flag before reading from custom array
879 ((*rom_)[OverworldCustomSubscreenOverlayArray + (index_ * 2) + 1] << 8);
880 } else {
881 // Fall back to hardcoded overlay values
882 subscreen_overlay_ = 0x00FF;
883 if (index_ == 0x00 || index_ == 0x01 || index_ == 0x08 || index_ == 0x09 ||
884 index_ == 0x40 || index_ == 0x41 || index_ == 0x48 || index_ == 0x49) {
885 // Add fog 2 to the lost woods and skull woods
886 subscreen_overlay_ = 0x009D;
887 } else if (index_ == 0x03 || index_ == 0x04 || index_ == 0x0B ||
888 index_ == 0x0C || index_ == 0x05 || index_ == 0x06 ||
889 index_ == 0x0D || index_ == 0x0E || index_ == 0x07) {
890 // Add the sky BG to LW death mountain
891 subscreen_overlay_ = 0x0095;
892 } else if (index_ == 0x43 || index_ == 0x44 || index_ == 0x4B ||
893 index_ == 0x4C || index_ == 0x45 || index_ == 0x46 ||
894 index_ == 0x4D || index_ == 0x4E || index_ == 0x47) {
895 // Add the lava to DW death mountain
896 subscreen_overlay_ = 0x009C;
897 } else if (index_ == 0x5B || index_ == 0x5C || index_ == 0x63 ||
898 index_ == 0x64) {
899 subscreen_overlay_ = 0x0096;
900 } else if (index_ == 0x80) {
901 // Add fog 1 to the master sword area
902 subscreen_overlay_ = 0x0097;
903 } else if (index_ == 0x88) {
904 // Add the triforce room curtains to the triforce room
905 subscreen_overlay_ = 0x0093;
906 }
907 }
908}
909
913
916 return 0x20;
917 } else if (parent_ >= kDarkWorldMapIdStart &&
919 return 0x21;
920 } else if (parent_ >= kSpecialWorldMapIdStart) {
921 // Special world maps - use appropriate graphics ID based on the specific
922 // map
923 if (parent_ == 0x88) {
924 return 0x24;
925 } else {
926 // Default special world graphics ID
927 return 0x20;
928 }
929 }
930 return 0x20;
931}
932
934 // Dungeon groups may be relocated independently of the overworld table.
939}
940
942 // JP's overworld area table differs from the dungeon's room table.
946}
947
962
967
968std::array<uint8_t, 8> OverworldMap::ResolveMainGraphicsGroup() const {
969 const int main_group_id = ComputeMainBlocksetId();
971 return game_data_->main_blockset_ids[main_group_id];
972 }
973 std::array<uint8_t, 8> sheets{};
974 for (int i = 0; i < 8; ++i) {
975 sheets[i] = (*rom_)[version_constants().kOverworldGfxGroups2 +
976 main_group_id * 8 + i];
977 }
978 return sheets;
979}
980
983 if (slot < 0 || slot >= 8) {
984 return result;
985 }
987 result.main_sheet = ResolveMainGraphicsGroup()[slot];
988 result.effective_sheet = ResolveStaticGraphics()[slot];
989 result.bottom_half_only = slot == 7;
992 } else if (slot >= 3 && slot <= 6 &&
993 ResolveAreaGraphicsGroup()[slot - 3] != 0) {
995 } else if (UsesSessionMainGraphicsGroup()) {
997 } else {
999 }
1000 return result;
1001}
1002
1003std::array<uint8_t, 4> OverworldMap::ResolveAreaGraphicsGroup() const {
1006 }
1007 std::array<uint8_t, 4> sheets{};
1008 for (int i = 0; i < 4; ++i) {
1009 sheets[i] = (*rom_)[version_constants().kOverworldGfxGroups1 +
1010 area_graphics_ * 4 + i];
1011 }
1012 return sheets;
1013}
1014
1015std::array<uint8_t, 4> OverworldMap::ResolveSpriteGraphicsGroup() const {
1016 const int set = sprite_graphics_[game_state_];
1018 return game_data_->spriteset_ids[set];
1019 }
1020 std::array<uint8_t, 4> sheets{};
1021 for (int i = 0; i < 4; ++i) {
1022 sheets[i] =
1023 (*rom_)[version_constants().kSpriteBlocksetPointer + set * 4 + i];
1024 }
1025 return sheets;
1026}
1027
1028std::array<uint8_t, 16> OverworldMap::ResolveStaticGraphics() const {
1029 std::array<uint8_t, 16> sheets{};
1030 const auto main = ResolveMainGraphicsGroup();
1031 const auto area = ResolveAreaGraphicsGroup();
1032 const auto sprites = ResolveSpriteGraphicsGroup();
1033 std::copy(main.begin(), main.end(), sheets.begin());
1034 for (int i = 0; i < 4; ++i) {
1035 if (area[i] != 0) {
1036 sheets[3 + i] = area[i];
1037 }
1038 sheets[12 + i] = static_cast<uint8_t>(sprites[i] + 0x73);
1039 }
1040 sheets[8] = 0x73;
1041 sheets[9] = 0x74;
1042 sheets[10] = 0x79;
1043 sheets[11] = 0x7A;
1044
1045 // v3 explicit tile groups replace the shared background assignment in full.
1046 // $FF inherits the world's custom defaults; $00 is an explicit sheet ID.
1048 const auto area_props = area_render_properties();
1049 const int world_offset = (parent_ & 0xC0) >> 3;
1050 for (int i = 0; i < 8; ++i) {
1051 const uint8_t custom_sheet = area_props.custom_gfx_ids[i];
1052 sheets[i] =
1053 custom_sheet == 0xFF
1054 ? (*rom_)[OverworldCustomDefaultGFXGroups + world_offset + i]
1055 : custom_sheet;
1056 }
1057 }
1058 return sheets;
1059}
1060
1061// For animating water tiles on the overworld map.
1062// We want to swap out static_graphics_[07] with the next sheet
1063// Usually it is 5A, so we make it 5B instead.
1064// There is a middle frame which contains tiles from the bottom half
1065// of the 5A sheet, so this will need some special manipulation to make work
1066// during the BuildBitmap step (or a new one specifically for animating).
1068 if (static_graphics_[7] == 0x5B) {
1069 static_graphics_[7] = 0x5A;
1070 } else {
1071 if (static_graphics_[7] == 0x59) {
1072 static_graphics_[7] = 0x58;
1073 }
1074 static_graphics_[7] = 0x5B;
1075 }
1076}
1077
1078// Resolves the animated tile sheet. Graphics slot 7 is a composite in game:
1079// the bottom half (door frames) comes from the area's sheet 7 and the top half
1080// holds frame 0 of the animated tiles (water/lava/clouds) decompressed from
1081// the animated sheet. Vanilla picks $59 on Death Mountain ($8A & $BF in
1082// 03/05/07) and $5B elsewhere; ZSCustomOverworld reads its AnimatedTable with
1083// $8A and falls back to the world's default sheet 7 for 0x00/0xFF.
1085 const auto version = OverworldVersionHelper::GetVersion(*rom_);
1086 uint8_t sheet = area_render_properties().animated_gfx;
1087 const bool custom_animated =
1088 descriptor_layout_ != nullptr ||
1089 (version != OverworldVersion::kVanilla &&
1090 (*rom_)[OverworldCustomAnimatedGFXEnabled] != 0);
1091 if (custom_animated) {
1092 if (sheet == 0x00 || sheet == 0xFF) {
1093 const int world_offset = (parent_ & 0xC0) >> 3;
1094 sheet = (*rom_)[OverworldCustomDefaultGFXGroups + world_offset + 7];
1095 }
1096 } else if (sheet == 0x00 || sheet == 0xFF) {
1097 sheet = IsDeathMountainArea(parent_) ? 0x59 : 0x5B;
1098 }
1099 animated_sheet_ = sheet;
1100}
1101
1107
1108namespace palette_internal {
1109
1110absl::Status SetColorsPalette(Rom& rom, GameData* game_data, int index,
1112 gfx::SnesPalette animated, gfx::SnesPalette aux1,
1114 gfx::SnesColor bgrcolor, gfx::SnesPalette spr,
1115 gfx::SnesPalette spr2,
1116 PaletteAddressMap* addresses,
1117 std::array<int, 3> palette_ids) {
1118 // Palettes infos, color 0 of a palette is always transparent (the arrays
1119 // contains 7 colors width wide) There is 16 color per line so 16*Y
1120
1121 // Left side of the palette - Main, Animated
1122 std::array<gfx::SnesColor, 256> new_palette = {};
1123 *addresses = {};
1124
1125 // Main Palette, Location 0,2 : 35 colors [7x5]
1126 int k = 0;
1127 for (int y = 2; y < 7; y++) {
1128 for (int x = 1; x < 8; x++) {
1129 new_palette[x + (16 * y)] = main[k];
1130 (*addresses)[x + 16 * y] = PaletteColorAddress{
1131 PaletteAddressGroup::kOverworldMain, palette_ids[0], k};
1132 k++;
1133 }
1134 }
1135
1136 // Animated Palette, Location 0,7 : 7colors
1137 for (int x = 1; x < 8; x++) {
1138 new_palette[(16 * 7) + (x)] = animated[(x - 1)];
1139 }
1140
1141 // Right side of the palette - Aux1, Aux2
1142
1143 // Aux1 Palette, Location 8,2 : 21 colors [7x3]
1144 k = 0;
1145 for (int y = 2; y < 5; y++) {
1146 for (int x = 9; x < 16; x++) {
1147 new_palette[x + (16 * y)] = aux1[k];
1148 (*addresses)[x + 16 * y] = PaletteColorAddress{
1149 PaletteAddressGroup::kOverworldAux, palette_ids[1], k};
1150 k++;
1151 }
1152 }
1153
1154 // Aux2 Palette, Location 8,5 : 21 colors [7x3]
1155 k = 0;
1156 for (int y = 5; y < 8; y++) {
1157 for (int x = 9; x < 16; x++) {
1158 new_palette[x + (16 * y)] = aux2[k];
1159 (*addresses)[x + 16 * y] = PaletteColorAddress{
1160 PaletteAddressGroup::kOverworldAux, palette_ids[2], k};
1161 k++;
1162 }
1163 }
1164
1165 // Hud Palette, Location 0,0 : 32 colors [16x2]
1166 for (int i = 0; i < 32; i++) {
1167 new_palette[i] = hud[i];
1168 }
1169
1170 // Hardcoded grass color (that might change to become invisible instead)
1171 for (int i = 0; i < 8; i++) {
1172 new_palette[(i * 16)] = bgrcolor;
1173 new_palette[(i * 16) + 8] = bgrcolor;
1174 }
1175
1176 // Sprite Palettes
1177 k = 0;
1178 for (int y = 8; y < 9; y++) {
1179 for (int x = 1; x < 8; x++) {
1180 new_palette[x + (16 * y)] = game_data->palette_groups.sprites_aux1[1][k];
1181 k++;
1182 }
1183 }
1184
1185 // Sprite Palettes
1186 k = 0;
1187 for (int y = 8; y < 9; y++) {
1188 for (int x = 9; x < 16; x++) {
1189 new_palette[x + (16 * y)] = game_data->palette_groups.sprites_aux3[0][k];
1190 k++;
1191 }
1192 }
1193
1194 // Sprite Palettes
1195 k = 0;
1196 for (int y = 9; y < 13; y++) {
1197 for (int x = 1; x < 16; x++) {
1198 new_palette[x + (16 * y)] =
1199 game_data->palette_groups.global_sprites[0][k];
1200 k++;
1201 }
1202 }
1203
1204 // Sprite Palettes
1205 k = 0;
1206 for (int y = 13; y < 14; y++) {
1207 for (int x = 1; x < 8; x++) {
1208 new_palette[x + (16 * y)] = spr[k];
1209 k++;
1210 }
1211 }
1212
1213 // Sprite Palettes
1214 k = 0;
1215 for (int y = 14; y < 15; y++) {
1216 for (int x = 1; x < 8; x++) {
1217 new_palette[x + (16 * y)] = spr2[k];
1218 k++;
1219 }
1220 }
1221
1222 // Sprite Palettes
1223 k = 0;
1224 for (int y = 15; y < 16; y++) {
1225 for (int x = 1; x < 16; x++) {
1226 new_palette[x + (16 * y)] = game_data->palette_groups.armors[0][k];
1227 k++;
1228 }
1229 }
1230
1231 for (int i = 0; i < 256; i++) {
1232 current[i] = new_palette[i];
1233 current[(i / 16) * 16].set_transparent(true);
1234 }
1235
1236 current.set_size(256);
1237 return absl::OkStatus();
1238}
1239} // namespace palette_internal
1240
1241absl::StatusOr<gfx::SnesPalette> OverworldMap::GetPalette(
1242 const gfx::PaletteGroup& palette_group, int index, int previous_index,
1243 int limit, int* resolved_index) {
1244 if (index == 255) {
1246 (previous_index * 4)];
1247 }
1248 if (index >= limit) {
1249 index = limit - 1;
1250 }
1251 if (resolved_index)
1252 *resolved_index = index;
1253 return palette_group[index];
1254}
1255
1257 palette_addresses_ = {};
1258 if (!game_data_) {
1260 return absl::OkStatus();
1261 }
1262
1263 uint8_t asm_version = OverworldVersionHelper::GetAsmVersion(*rom_);
1265
1266 int previous_pal_id = 0;
1267 int previous_spr_pal_id = 0;
1268
1269 if (index_ > 0 && (descriptor_layout_ == nullptr || parent_ > 0)) {
1270 // Load previous palette ID based on ASM version
1271 if (descriptor_layout_ != nullptr) {
1273 rom_->size(), 31, parent_ - 1);
1274 if (!previous) {
1275 return absl::FailedPreconditionError(
1276 "Lyra preview previous palette unavailable");
1277 }
1278 previous_pal_id = (*previous)[0];
1280 auto previous_sprite = rom::LyraLayoutRecord(
1281 *descriptor_layout_, rom_->data(), rom_->size(), 25,
1283 if (!previous_sprite) {
1284 return absl::FailedPreconditionError(
1285 "Lyra preview previous sprite palette unavailable");
1286 }
1287 previous_spr_pal_id = (*previous_sprite)[0];
1288 }
1289 } else if (OverworldVersionHelper::SupportsAreaEnum(version)) {
1290 // v3 uses expanded palette table
1291 previous_pal_id = (*rom_)[kOverworldPalettesScreenToSetNew + parent_ - 1];
1292 } else {
1293 previous_pal_id = (*rom_)[kOverworldMapPaletteIds + parent_ - 1];
1294 }
1295
1296 if (descriptor_layout_ == nullptr) {
1297 previous_spr_pal_id = (*rom_)[kOverworldSpritePaletteIds + parent_ - 1];
1298 }
1299 }
1300
1301 const int render_area_palette = std::min((int)area_palette_, 0xA3);
1302
1303 uint8_t pal0 = 0;
1304 uint8_t pal1 = (*rom_)[version_constants().kOverworldMapPaletteGroup +
1305 (render_area_palette * 4)];
1306 uint8_t pal2 = (*rom_)[version_constants().kOverworldMapPaletteGroup +
1307 (render_area_palette * 4) + 1];
1308 uint8_t pal3 = (*rom_)[version_constants().kOverworldMapPaletteGroup +
1309 (render_area_palette * 4) + 2];
1310 uint8_t pal4 = (*rom_)[kOverworldSpritePaletteGroup +
1312 uint8_t pal5 = (*rom_)[kOverworldSpritePaletteGroup +
1313 (sprite_palette_[game_state_] * 2) + 1];
1314
1315 auto& grass_pal_group = game_data_->palette_groups.grass;
1316 auto bgr = grass_pal_group[0][0];
1317
1318 // Handle 0xFF palette references (use previous palette)
1319 if (pal1 == 0xFF) {
1321 (previous_pal_id * 4)];
1322 }
1323
1324 if (pal2 == 0xFF) {
1326 (previous_pal_id * 4) + 1];
1327 }
1328
1329 if (pal3 == 0xFF) {
1331 (previous_pal_id * 4) + 2];
1332 }
1333
1334 std::array<int, 3>
1335 palette_ids{}; // Main, aux1, aux2 after all loader fallbacks.
1336 auto& ow_aux_pal_group = game_data_->palette_groups.overworld_aux;
1338 gfx::SnesPalette aux1,
1339 GetPalette(ow_aux_pal_group, pal1, previous_pal_id, 20, &palette_ids[1]));
1341 gfx::SnesPalette aux2,
1342 GetPalette(ow_aux_pal_group, pal2, previous_pal_id, 20, &palette_ids[2]));
1343
1344 // Set background color based on world type and area-specific settings
1345 bool use_area_specific_bg =
1346 descriptor_layout_ != nullptr ||
1348 (*rom_)[OverworldCustomAreaSpecificBGEnabled] != 0x00);
1349 if (use_area_specific_bg) {
1350 // Rendering inherits the parent; the raw child color stays saveable.
1352 } else {
1353 // Use default world-based background colors
1355 bgr = grass_pal_group[0][0]; // LW
1356 } else if (parent_ >= kDarkWorldMapIdStart &&
1358 bgr = grass_pal_group[0][1]; // DW
1359 } else if (parent_ >= 128 && parent_ < kNumOverworldMaps) {
1360 bgr = grass_pal_group[0][2]; // SW
1361 }
1362 }
1363
1364 // Use main palette from the overworld map data (matches ZScream logic).
1365 // Child screens use the area parent's entry, like the game ($8A).
1366 if (version == OverworldVersion::kVanilla) {
1367 // Vanilla ROMs never write main_palette_ elsewhere; ensure world defaults
1369 pal0 = main_palette_;
1370 } else {
1372 }
1373
1374 auto& ow_main_pal_group = game_data_->palette_groups.overworld_main;
1376 GetPalette(ow_main_pal_group, pal0, previous_pal_id, 255,
1377 &palette_ids[0]));
1378 auto& ow_animated_pal_group = game_data_->palette_groups.overworld_animated;
1380 GetPalette(ow_animated_pal_group, std::min((int)pal3, 13),
1381 previous_pal_id, 14));
1382
1383 auto& hud_pal_group = game_data_->palette_groups.hud;
1384 gfx::SnesPalette hud = hud_pal_group[0];
1385
1386 // Handle 0xFF sprite palette references (use previous sprite palette)
1387 if (pal4 == 0xFF) {
1388 pal4 = (*rom_)[kOverworldSpritePaletteGroup + (previous_spr_pal_id * 2)];
1389 }
1390
1391 if (pal4 == 0xFF) {
1392 pal4 = 0; // Fallback to 0 if still 0xFF
1393 }
1394
1395 if (pal5 == 0xFF) {
1396 pal5 =
1397 (*rom_)[kOverworldSpritePaletteGroup + (previous_spr_pal_id * 2) + 1];
1398 }
1399
1400 if (pal5 == 0xFF) {
1401 pal5 = 0; // Fallback to 0 if still 0xFF
1402 }
1403
1406 previous_spr_pal_id, 24));
1409 previous_spr_pal_id, 24));
1410
1412 *rom_, game_data_, parent_, current_palette_, main, animated, aux1, aux2,
1413 hud, bgr, spr, spr2, &palette_addresses_, palette_ids));
1414
1415 // current_palette_ is this map's resolved palette. Do not populate the
1416 // unused process-wide GfxContext cache here: maps load concurrently, and
1417 // unsynchronized insertions can corrupt its unordered_map during rehash.
1418 return absl::OkStatus();
1419}
1420
1422 // Load overlays based on ROM version
1424 // Vanilla ROM - load overlay from overlay pointers
1425 return LoadVanillaOverlayData();
1426 }
1427
1428 // Custom overworld ROM - use the area's subscreen overlay (parent's entry
1429 // for child screens, like the game)
1431 has_overlay_ = (overlay_id_ != 0x00FF);
1432 overlay_data_.clear();
1433 return absl::OkStatus();
1434}
1435
1437 // Load vanilla overlay for this map (interactive overlays for revealing
1438 // holes/changing elements)
1439 int address = (kOverlayPointersBank << 16) +
1440 ((*rom_)[kOverlayPointers + (index_ * 2) + 1] << 8) +
1441 (*rom_)[kOverlayPointers + (index_ * 2)];
1442
1443 // Convert SNES address to PC address
1444 address = ((address & 0x7F0000) >> 1) | (address & 0x7FFF);
1445
1446 // Check if custom overlay code is present
1447 if ((*rom_)[kOverlayData1] == 0x6B) {
1448 // Use custom overlay data pointer
1449 address = ((*rom_)[kOverlayData2 + 2 + (index_ * 3)] << 16) +
1450 ((*rom_)[kOverlayData2 + 1 + (index_ * 3)] << 8) +
1451 (*rom_)[kOverlayData2 + (index_ * 3)];
1452 address = ((address & 0x7F0000) >> 1) | (address & 0x7FFF);
1453 }
1454
1455 // Validate address
1456 if (address >= rom_->size()) {
1457 has_overlay_ = false;
1458 overlay_id_ = 0;
1459 overlay_data_.clear();
1460 return absl::OkStatus();
1461 }
1462
1463 // Parse overlay data (interactive overlays)
1464 overlay_data_.clear();
1465 uint8_t b = (*rom_)[address];
1466
1467 // Parse overlay commands until we hit END (0x60)
1468 while (b != 0x60 && address < rom_->size()) {
1469 overlay_data_.push_back(b);
1470
1471 // Handle different overlay commands
1472 switch (b) {
1473 case 0xA9: // LDA #$
1474 if (address + 2 < rom_->size()) {
1475 overlay_data_.push_back((*rom_)[address + 1]);
1476 overlay_data_.push_back((*rom_)[address + 2]);
1477 address += 3;
1478 } else {
1479 address++;
1480 }
1481 break;
1482 case 0xA2: // LDX #$
1483 if (address + 2 < rom_->size()) {
1484 overlay_data_.push_back((*rom_)[address + 1]);
1485 overlay_data_.push_back((*rom_)[address + 2]);
1486 address += 3;
1487 } else {
1488 address++;
1489 }
1490 break;
1491 case 0x8D: // STA $xxxx
1492 if (address + 3 < rom_->size()) {
1493 overlay_data_.push_back((*rom_)[address + 1]);
1494 overlay_data_.push_back((*rom_)[address + 2]);
1495 overlay_data_.push_back((*rom_)[address + 3]);
1496 address += 4;
1497 } else {
1498 address++;
1499 }
1500 break;
1501 case 0x9D: // STA $xxxx,x
1502 if (address + 3 < rom_->size()) {
1503 overlay_data_.push_back((*rom_)[address + 1]);
1504 overlay_data_.push_back((*rom_)[address + 2]);
1505 overlay_data_.push_back((*rom_)[address + 3]);
1506 address += 4;
1507 } else {
1508 address++;
1509 }
1510 break;
1511 case 0x8F: // STA $xxxxxx
1512 if (address + 4 < rom_->size()) {
1513 overlay_data_.push_back((*rom_)[address + 1]);
1514 overlay_data_.push_back((*rom_)[address + 2]);
1515 overlay_data_.push_back((*rom_)[address + 3]);
1516 overlay_data_.push_back((*rom_)[address + 4]);
1517 address += 5;
1518 } else {
1519 address++;
1520 }
1521 break;
1522 case 0x1A: // INC A
1523 address++;
1524 break;
1525 case 0x4C: // JMP
1526 if (address + 3 < rom_->size()) {
1527 overlay_data_.push_back((*rom_)[address + 1]);
1528 overlay_data_.push_back((*rom_)[address + 2]);
1529 overlay_data_.push_back((*rom_)[address + 3]);
1530 address += 4;
1531 } else {
1532 address++;
1533 }
1534 break;
1535 default:
1536 address++;
1537 break;
1538 }
1539
1540 if (address < rom_->size()) {
1541 b = (*rom_)[address];
1542 } else {
1543 break;
1544 }
1545 }
1546
1547 // Add the END command if we found it
1548 if (b == 0x60) {
1549 overlay_data_.push_back(0x60);
1550 }
1551
1552 // Set overlay ID based on map index (simplified)
1554 has_overlay_ = !overlay_data_.empty();
1555
1556 return absl::OkStatus();
1557}
1558
1560 // Entries 0-15 are the static slots; entry 16 is the animated sheet whose
1561 // first frame CopyAnimatedSheetIntoSlot7() copies into slot 7.
1562 return i < 16 ? static_graphics_[i] : animated_sheet_;
1563}
1564
1567 for (int i = 0; i < kNumSourceSheets; i++) {
1568 const uint16_t sheet = SourceSheetForRevision(i);
1570 game_data_ != nullptr && sheet != 0 &&
1571 graphics_sheet_overrides_.count(sheet) == 0
1573 : 0;
1574 }
1575}
1576
1578 if (game_data_ == nullptr) {
1579 return false;
1580 }
1583 return true;
1584 }
1585 for (int i = 0; i < kNumSourceSheets; i++) {
1586 const uint16_t sheet = SourceSheetForRevision(i);
1587 if (source_sheet_revisions_[i] != 0 && sheet != 0 &&
1588 graphics_sheet_overrides_.count(sheet) == 0 &&
1590 return true;
1591 }
1592 }
1593 return false;
1594}
1595
1596void OverworldMap::ProcessGraphicsBuffer(int index, int static_graphics_offset,
1597 int size, const uint8_t* all_gfx) {
1598 if (const auto it = graphics_sheet_overrides_.find(
1599 static_cast<uint16_t>(static_graphics_offset));
1600 it != graphics_sheet_overrides_.end() &&
1601 it->second.size() == static_cast<size_t>(size)) {
1602 all_gfx = it->second.data();
1603 static_graphics_offset = 0;
1604 } else if (static_graphics_offset * size + size >
1605 static_cast<int>(game_data_ ? game_data_->graphics_buffer.size()
1606 : 0)) {
1607 // Fill with zeros if out of bounds
1608 for (int i = 0; i < size; i++) {
1609 current_gfx_[(index * size) + i] = 0x00;
1610 }
1611 return;
1612 }
1613 for (int i = 0; i < size; i++) {
1614 auto byte = all_gfx[i + (static_graphics_offset * size)];
1615 switch (index) {
1616 case 0:
1617 case 3:
1618 case 4:
1619 case 5:
1620 byte += 0x88;
1621 break;
1622 }
1623 current_gfx_[(index * size) + i] = byte;
1624 }
1625}
1626
1628 // Rebuild ordinary sources so cleared previews cannot leave stale pixels.
1629 current_gfx_.assign(0x10000, 0x00);
1630
1631 if (!game_data_) {
1632 // Headless/tests: allow map builds without graphics by keeping zeroed data.
1634 return absl::OkStatus();
1635 }
1636
1637 // Process the 8 main graphics sheets (slots 0-7)
1638 for (int i = 0; i < 8; i++) {
1639 if (static_graphics_[i] != 0) {
1641 game_data_->graphics_buffer.data());
1642 }
1643 }
1644
1645 // Process sprite graphics (slots 8-15)
1646 for (int i = 8; i < 16; i++) {
1647 if (static_graphics_[i] != 0) {
1649 game_data_->graphics_buffer.data());
1650 }
1651 }
1652
1653 // The top of slot 7 shows frame 0 of the animated tiles (ZScream does the
1654 // same with its StaticGFX[16]); the bottom keeps the area's sheet 7.
1656
1657 // Remember what each slot (and the animated sheet copied into slot 7) was
1658 // built from, so an unsaved sheet edit can be detected
1659 // (SourceSheetsChanged).
1662
1663 return absl::OkStatus();
1664}
1665
1667 if (!game_data_ || animated_sheet_ == 0) {
1668 return;
1669 }
1670 const size_t dst = 7 * 0x1000;
1671 // A preview override (SetGraphicsSheetOverrides) wins over the store.
1672 if (const auto it = graphics_sheet_overrides_.find(animated_sheet_);
1673 it != graphics_sheet_overrides_.end()) {
1674 if (it->second.size() >= kAnimatedSheetSlotBytes &&
1675 dst + kAnimatedSheetSlotBytes <= current_gfx_.size()) {
1676 std::copy_n(it->second.begin(), kAnimatedSheetSlotBytes,
1677 current_gfx_.begin() + dst);
1678 }
1679 return;
1680 }
1681 const size_t src = static_cast<size_t>(animated_sheet_) * 0x1000;
1683 dst + kAnimatedSheetSlotBytes > current_gfx_.size()) {
1684 return;
1685 }
1686 std::copy_n(game_data_->graphics_buffer.begin() + src,
1687 kAnimatedSheetSlotBytes, current_gfx_.begin() + dst);
1688}
1689
1690absl::Status OverworldMap::BuildTiles16Gfx(std::vector<gfx::Tile16>& tiles16,
1691 int count) {
1692 constexpr size_t kBlocksetBytes = 0x100000;
1693 constexpr size_t kTilesetBytes = 0x10000;
1694 if (current_blockset_.size() == 0)
1695 current_blockset_.resize(kBlocksetBytes, 0x00);
1696 if (current_gfx_.size() < kTilesetBytes)
1697 current_gfx_.resize(kTilesetBytes, 0x00);
1698
1699 // Each row of 8 tile16s takes 0x800 bytes of the 128px-wide blockset.
1700 const int capacity = static_cast<int>(current_blockset_.size() / 0x800) * 8;
1701 count = std::min({count, static_cast<int>(tiles16.size()), capacity});
1702
1703 const int offsets[] = {0x00, 0x08, 0x400, 0x408};
1704 const uint8_t* gfx = current_gfx_.data();
1705 const size_t gfx_size = current_gfx_.size();
1706 uint8_t* blockset = current_blockset_.data();
1707
1708 for (int i = 0; i < count; i++) {
1709 const size_t tile_base =
1710 static_cast<size_t>(i / 8) * 0x800 + static_cast<size_t>(i % 8) * 0x10;
1711 for (int tile = 0; tile < 4; tile++) {
1712 const gfx::TileInfo& info = tiles16[i].tiles_info[tile];
1713 const size_t src_base = static_cast<size_t>(info.id_ / 0x10) * 0x400 +
1714 static_cast<size_t>(info.id_ % 0x10) * 0x08;
1715 uint8_t* dst_tile = blockset + tile_base + offsets[tile];
1716 if (src_base + 7 * 0x80 + 8 > gfx_size) {
1717 for (int y = 0; y < 8; ++y) {
1718 std::fill_n(dst_tile + y * 0x80, 8, 0x00);
1719 }
1720 continue;
1721 }
1722 const uint8_t palette = static_cast<uint8_t>(info.palette_ * 0x10);
1723 for (int y = 0; y < 8; ++y) {
1724 const int my = info.vertical_mirror_ != 0 ? 7 - y : y;
1725 const uint8_t* src = gfx + src_base + y * 0x80;
1726 uint8_t* dst = dst_tile + my * 0x80;
1727 if (info.horizontal_mirror_ != 0) {
1728 for (int x = 0; x < 8; ++x) {
1729 dst[7 - x] = static_cast<uint8_t>((src[x] & 0x0F) + palette);
1730 }
1731 } else {
1732 for (int x = 0; x < 8; ++x) {
1733 dst[x] = static_cast<uint8_t>((src[x] & 0x0F) + palette);
1734 }
1735 }
1736 }
1737 }
1738 }
1739
1740 return absl::OkStatus();
1741}
1742
1743absl::Status OverworldMap::BuildBitmap(OverworldBlockset& world_blockset) {
1744 bitmap_data_.assign(0x40000, 0x00);
1745
1746 // BuildBitmap is used by both full map builds and editor refresh paths.
1747 // Refresh paths can run after LRU eviction reset runtime fields, so derive
1748 // the world/local coordinate from the stable map id instead of trusting
1749 // world_ to still be populated.
1751 const int local_index = index_ - (world_ * kNumMapsPerWorld);
1752 if (local_index < 0) {
1753 return absl::InvalidArgumentError("Invalid overworld map index");
1754 }
1755
1756 int superY = local_index / 0x08;
1757 int superX = local_index - (superY * 0x08);
1758
1759 for (int y = 0; y < 0x20; y++) {
1760 for (int x = 0; x < 0x20; x++) {
1761 auto xt = x + (superX * 0x20);
1762 auto yt = y + (superY * 0x20);
1763 if (xt < 0 || yt < 0 || xt >= static_cast<int>(world_blockset.size()) ||
1764 yt >= static_cast<int>(world_blockset[xt].size())) {
1765 return absl::InvalidArgumentError(
1766 "Overworld blockset is too small for map bitmap build");
1767 }
1768 gfx::CopyTile8bpp16((x * 0x10), (y * 0x10), world_blockset[xt][yt],
1770 }
1771 }
1772 static std::atomic<uint64_t> next_bitmap_serial{1};
1773 bitmap_serial_ = next_bitmap_serial.fetch_add(1);
1774 return absl::OkStatus();
1775}
1776
1778 const OverworldBlockset& overlay_world_blockset, int overlay_screen,
1779 bool background, std::vector<uint8_t>* out) const {
1780 if (out == nullptr) {
1781 return absl::InvalidArgumentError("Overlay layer output is null");
1782 }
1783 constexpr int kSize = 0x200;
1784 if (bitmap_data_.size() < static_cast<size_t>(kSize) * kSize ||
1785 current_blockset_.empty()) {
1786 return absl::FailedPreconditionError(
1787 "Map must be built before its overlay layer");
1788 }
1789 const int local = overlay_screen & 0x3F;
1790 const int super_x = local % 8;
1791 const int super_y = local / 8;
1792
1793 out->assign(static_cast<size_t>(kSize) * kSize, 0);
1794 std::vector<uint8_t> tile_pixels(static_cast<size_t>(kSize) * kSize, 0);
1795 for (int y = 0; y < 0x20; ++y) {
1796 for (int x = 0; x < 0x20; ++x) {
1797 const int xt = x + super_x * 0x20;
1798 const int yt = y + super_y * 0x20;
1799 if (xt >= static_cast<int>(overlay_world_blockset.size()) ||
1800 yt >= static_cast<int>(overlay_world_blockset[xt].size())) {
1801 return absl::InvalidArgumentError(
1802 "Overlay blockset is too small for the overlay screen");
1803 }
1804 gfx::CopyTile8bpp16(x * 0x10, y * 0x10, overlay_world_blockset[xt][yt],
1805 tile_pixels, current_blockset_);
1806 }
1807 }
1808 for (size_t i = 0; i < tile_pixels.size(); ++i) {
1809 const uint8_t overlay = tile_pixels[i];
1810 if (IsOverworldBackdropPixel(overlay)) {
1811 continue;
1812 }
1813 if (background && !IsOverworldBackdropPixel(bitmap_data_[i])) {
1814 continue;
1815 }
1816 (*out)[i] = overlay;
1817 }
1818 return absl::OkStatus();
1819}
1820
1821} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
static Flags & get()
Definition features.h:119
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void set_size(size_t size)
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
uint64_t Revision(uint16_t sheet) const
Represents a single Overworld map screen.
OverworldMainSlotProvenance MainSlotProvenance(int slot) const
void ProcessGraphicsBuffer(int index, int static_graphics_offset, int size, const uint8_t *all_gfx)
zelda3_version_pointers version_constants() const
const rom::LyraLayoutInspection * descriptor_layout_
std::array< uint8_t, 8 > raw_area_info_
std::vector< uint8_t > current_gfx_
std::array< uint64_t, kNumSourceSheets > source_sheet_revisions_
std::vector< uint8_t > current_blockset_
std::array< bool, 4 > mosaic_expanded_
absl::Status LoadVanillaOverlayData()
std::vector< uint8_t > bitmap_data_
bool UsesSessionAreaGraphicsGroup() const
std::array< uint8_t, 8 > loaded_area_info_
std::map< uint16_t, std::vector< uint8_t > > graphics_sheet_overrides_
std::array< uint8_t, 16 > ResolveStaticGraphics() const
std::array< uint8_t, 4 > ResolveSpriteGraphicsGroup() const
void SetupCustomTileset(uint8_t asm_version)
absl::Status BuildTiles16Gfx(std::vector< gfx::Tile16 > &tiles16, int count)
bool UsesSessionSpriteGraphicsGroup() const
absl::Status BuildMap(int count, int game_state, int world, std::vector< gfx::Tile16 > &tiles16, OverworldBlockset &world_blockset)
std::array< uint8_t, 3 > sprite_graphics_
bool UsesSessionMainGraphicsGroup() const
static constexpr int kNumSourceSheets
OverworldRoomsetSource roomset_source() const
uint8_t ComputeWorldBasedMainPalette() const
absl::Status BuildBitmap(OverworldBlockset &world_blockset)
std::array< uint8_t, 3 > sprite_palette_
std::array< uint8_t, 4 > area_music_
static absl::StatusOr< OverworldMap > CreateLyraPreview(int index, Rom *rom, GameData *game_data, const rom::LyraLayoutInspection *layout)
std::array< uint8_t, 16 > static_graphics_
std::array< uint8_t, 8 > custom_gfx_ids_
const GameData * game_data() const
absl::Status BuildMapWithCache(int count, int game_state, int world, std::vector< gfx::Tile16 > &tiles16, OverworldBlockset &world_blockset, const std::vector< uint8_t > *cached_tileset, const std::vector< uint8_t > *cached_tile16_blockset=nullptr)
Build map with optional cached tileset for performance.
absl::Status LoadLyraPreviewProperties()
PaletteAddressMap palette_addresses_
std::array< uint8_t, 4 > ResolveAreaGraphicsGroup() const
void InheritAreaProperties(const OverworldMap &area_parent)
OverworldTilesetKey tileset_key() const
std::vector< uint8_t > overlay_data_
void UseCachedTileset(const std::vector< uint8_t > &cached_gfx)
Use a pre-computed tileset from cache instead of rebuilding.
std::optional< AreaRenderProperties > inherited_area_
auto area_specific_bg_color() const
auto sprite_palette(int i) const
gfx::SnesPalette current_palette_
uint16_t SourceSheetForRevision(int i) const
absl::Status BuildSubscreenOverlayLayer(const OverworldBlockset &overlay_world_blockset, int overlay_screen, bool background, std::vector< uint8_t > *out) const
Build this area's subscreen overlay layer (512x512, 8bpp).
absl::StatusOr< gfx::SnesPalette > GetPalette(const gfx::PaletteGroup &group, int index, int previous_index, int limit, int *resolved_index=nullptr)
AreaGraphicsPreview graphics_preview_
std::array< uint8_t, 8 > ResolveMainGraphicsGroup() const
AreaRenderProperties area_render_properties() const
static bool SupportsCustomBGColors(OverworldVersion version)
Check if ROM supports custom background colors per area (v2+)
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
static uint8_t GetAsmVersion(const Rom &rom)
Get raw ASM version byte from ROM.
static bool SupportsAreaEnum(OverworldVersion version)
Check if ROM supports area enum system (v3+ only)
static bool SupportsExpandedSpace(OverworldVersion version)
Check if ROM uses expanded ROM space for overworld data.
static bool SupportsCustomTileGFX(OverworldVersion version)
Check if ROM supports custom tile GFX groups (v3+)
int main(int argc, char **argv)
Definition emu.cc:43
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void CopyTile8bpp16(int x, int y, int tile, std::vector< uint8_t > &bitmap, const std::vector< uint8_t > &blockset)
Definition snes_tile.cc:424
std::optional< std::span< const uint8_t > > LyraLayoutRecord(const LyraLayoutInspection &layout, const uint8_t *image, size_t size, uint16_t table_id, size_t index)
absl::Status SetColorsPalette(Rom &rom, GameData *game_data, int index, gfx::SnesPalette &current, gfx::SnesPalette main, gfx::SnesPalette animated, gfx::SnesPalette aux1, gfx::SnesPalette aux2, gfx::SnesPalette hud, gfx::SnesColor bgrcolor, gfx::SnesPalette spr, gfx::SnesPalette spr2, PaletteAddressMap *addresses, std::array< int, 3 > palette_ids)
Zelda 3 specific classes and functions.
constexpr int kAreaGfxIdPtr
Definition overworld.h:124
int GetOverworldMapParentIdExpanded()
constexpr int OverworldCustomTileGFXGroupEnabled
constexpr int OverworldCustomAreaSpecificBGEnabled
constexpr int kOverworldSpritePaletteGroup
Definition overworld.h:116
constexpr int kNumMapsPerWorld
Definition overworld.h:270
constexpr int kOverworldSpriteset
Definition overworld.h:117
constexpr int kOverworldScreenSize
Definition overworld.h:150
constexpr int kOverlayData1
constexpr int kSpecialWorldMapIdStart
constexpr int OverworldCustomMosaicArray
void ApplyGraphicsPreview(const AreaGraphicsPreview &preview, std::vector< uint8_t > &graphics)
constexpr int OverworldCustomDefaultGFXGroups
constexpr int OverworldCustomAnimatedGFXEnabled
constexpr int OverworldCustomMainPaletteEnabled
constexpr int kNumOverworldMaps
Definition common.h:85
constexpr int OverworldCustomMainPaletteArray
constexpr int kAnimatedSheetSlotBytes
constexpr int kOverworldSpecialSpritePaletteExpandedTemp
constexpr int kOverworldSpecialSpriteGfxGroupExpandedTemp
constexpr int kOverworldMusicBeginning
Definition overworld.h:127
std::vector< std::vector< uint16_t > > OverworldBlockset
Represents tile32 data for the overworld.
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int kOverworldMusicDarkWorld
Definition overworld.h:131
constexpr int kOverlayPointersBank
std::array< uint8_t, 17 > OverworldTilesetKey
constexpr int kOverworldSpecialPalGroup
Definition overworld.h:119
constexpr int kOverworldSpritePaletteIds
Definition overworld.h:115
constexpr uint32_t kEntranceGfxGroup
Definition game_data.h:38
constexpr int kOverworldMusicAgahnim
Definition overworld.h:130
bool IsOverworldBackdropPixel(uint8_t index)
constexpr int kOverworldMapParentId
Definition overworld.h:147
@ kZSCustomV2
Parent system, BG colors, main palettes.
@ kVanilla
0xFF in ROM, no ZScream ASM applied
@ kZSCustomV3
Area enum, wide/tall areas, all features.
constexpr int kOverworldPalettesScreenToSetNew
constexpr int kOverworldMusicMasterSword
Definition overworld.h:129
constexpr int kOverworldMusicZelda
Definition overworld.h:128
constexpr int kOverworldMessageIds
Definition overworld.h:125
constexpr int kOverlayData2
constexpr int kOverworldSpecialSpritePalette
constexpr int OverworldCustomAnimatedGFXArray
constexpr int kDarkWorldMapIdStart
constexpr int OverworldCustomMosaicEnabled
int WorldForOverworldMap(int map_index)
Definition overworld.h:355
constexpr int kOverworldSpecialSpriteGFXGroup
constexpr int OverworldCustomTileGFXGroupArray
constexpr int OverworldCustomSubscreenOverlayEnabled
constexpr int OverworldCustomAreaSpecificBGPalette
int GetOverworldMessagesExpanded()
constexpr int kOverlayPointers
int LegacyScreenSizeTableIndexForMap(int map_index)
Definition overworld.h:385
constexpr int kOverworldMapPaletteIds
Definition overworld.h:114
constexpr int kOverworldSpecialGfxGroup
Definition overworld.h:118
constexpr int OverworldCustomSubscreenOverlayArray
std::array< std::optional< PaletteColorAddress >, 128 > PaletteAddressMap
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Represents a group of palettes.
Per-area render settings the game reads with the area id ($8A).
std::array< uint8_t, 8 > custom_gfx_ids
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:99
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97
uint32_t gfx_main_group_address
Definition game_data.h:102
std::vector< uint8_t > & graphics_buffer
Legacy name for sheet_store.mutable_pixels().
uint32_t kOverworldMapPaletteGroup
Definition zelda.h:98
uint32_t kOverworldGfxGroups1
Definition zelda.h:94
uint32_t kSpriteBlocksetPointer
Definition zelda.h:109
uint32_t kOverworldGfxGroups2
Definition zelda.h:95