yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
map_properties.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <cstring>
7#include <utility>
8#include <vector>
9
10#include "absl/status/statusor.h"
11#include "absl/strings/str_format.h"
17#include "app/gui/core/color.h"
18#include "app/gui/core/icons.h"
19#include "app/gui/core/input.h"
25#include "imgui/imgui.h"
26#include "util/macro.h"
29
30namespace yaze {
31namespace editor {
32
33using ImGui::BeginTable;
34// HOVER_HINT is defined in util/macro.h
35using ImGui::Separator;
36using ImGui::TableNextColumn;
37using ImGui::Text;
38
39// Using centralized UI constants
40namespace {
41
42bool IsValidMapId(int map_id) {
43 return map_id >= 0 && map_id < zelda3::kNumOverworldMaps;
44}
45
46int EffectivePropertyMapId(zelda3::Overworld* overworld, int map_id) {
47 if (!overworld || !IsValidMapId(map_id)) {
48 return map_id;
49 }
50 auto* map = overworld->mutable_overworld_map(map_id);
51 if (!map) {
52 return map_id;
53 }
54 const int parent = map->parent();
55 if (!IsValidMapId(parent)) {
56 return map_id;
57 }
58 return parent;
59}
60
61absl::Status CheckFieldSupported(const Rom* rom,
62 const OverworldPropertyEdit& edit) {
63 if (!rom || !rom->is_loaded()) {
64 return absl::FailedPreconditionError("ROM is not loaded");
65 }
66 const auto version = zelda3::OverworldVersionHelper::GetVersion(*rom);
67 switch (edit.field) {
69 if ((edit.value == static_cast<int>(zelda3::AreaSizeEnum::WideArea) ||
70 edit.value == static_cast<int>(zelda3::AreaSizeEnum::TallArea)) &&
72 return absl::FailedPreconditionError(
73 "Wide and Tall areas require ZSCustomOverworld v3+");
74 }
75 return absl::OkStatus();
80 return absl::FailedPreconditionError(
81 "This field requires ZSCustomOverworld v2+");
82 }
83 return absl::OkStatus();
86 return absl::FailedPreconditionError(
87 "Animated GFX requires ZSCustomOverworld v3+");
88 }
89 return absl::OkStatus();
92 return absl::FailedPreconditionError(
93 "Custom tile graphics require ZSCustomOverworld v1+");
94 }
95 return absl::OkStatus();
98 return absl::FailedPreconditionError(
99 "Visual effect editing requires ZSCustomOverworld v1+");
100 }
101 return absl::OkStatus();
102 default:
103 return absl::OkStatus();
104 }
105}
106
107int MusicRomAddressForMapState(int map_id, int state) {
108 if (map_id >= 0 && map_id < zelda3::kDarkWorldMapIdStart) {
109 switch (state) {
110 case 0:
111 return zelda3::kOverworldMusicBeginning + map_id;
112 case 1:
113 return zelda3::kOverworldMusicZelda + map_id;
114 case 2:
116 case 3:
117 return zelda3::kOverworldMusicAgahnim + map_id;
118 default:
119 return -1;
120 }
121 }
122 if (map_id >= zelda3::kDarkWorldMapIdStart &&
123 map_id < zelda3::kSpecialWorldMapIdStart && state == 0) {
126 }
127 return -1;
128}
129
130bool WriteRomByteIfValid(Rom* rom, int address, uint8_t value) {
131 if (!rom || address < 0 || static_cast<size_t>(address) >= rom->size()) {
132 return false;
133 }
134 (*rom)[address] = value;
135 return true;
136}
137
138void WriteRomWordIfValid(Rom* rom, int address, uint16_t value) {
139 if (!rom || address < 0 || static_cast<size_t>(address + 1) >= rom->size()) {
140 return;
141 }
142 (*rom)[address] = value & 0xFF;
143 (*rom)[address + 1] = (value >> 8) & 0xFF;
144}
145
147 const zelda3::Overworld& overworld, int map_id) {
149 if (!IsValidMapId(map_id)) {
150 return clipboard;
151 }
152
153 const auto* selected_map = overworld.overworld_map(map_id);
154 if (!selected_map) {
155 return clipboard;
156 }
157
158 int source_map_id = map_id;
159 if (IsValidMapId(selected_map->parent())) {
160 source_map_id = selected_map->parent();
161 }
162 const auto* map = overworld.overworld_map(source_map_id);
163 if (!map) {
164 return clipboard;
165 }
166
167 clipboard.valid = true;
168 clipboard.source_map_id = source_map_id;
169 clipboard.area_size = static_cast<int>(map->area_size());
170 clipboard.area_graphics = map->area_graphics();
171 clipboard.area_palette = map->area_palette();
172 clipboard.main_palette = map->main_palette();
173 clipboard.animated_graphics = map->animated_gfx();
174 clipboard.message_id = map->message_id();
175 clipboard.subscreen_overlay = map->subscreen_overlay();
176 clipboard.area_specific_bg_color = map->area_specific_bg_color();
177 clipboard.mosaic = *const_cast<zelda3::OverworldMap*>(map)->mutable_mosaic();
178 clipboard.mosaic_expanded = map->mosaic_expanded();
179 for (int i = 0; i < 3; ++i) {
180 clipboard.sprite_graphics[i] = map->sprite_graphics(i);
181 clipboard.sprite_palette[i] = map->sprite_palette(i);
182 }
183 for (int i = 0; i < 4; ++i) {
184 clipboard.music[i] = map->area_music(i);
185 }
186 for (int i = 0; i < 8; ++i) {
187 clipboard.custom_tilesets[i] = map->custom_tileset(i);
188 }
189 return clipboard;
190}
191
192} // namespace
193
197
199 return std::clamp(game_state_ ? *game_state_ : fallback, 0, 2);
200}
201
203 const int clamped_state = std::clamp(game_state, 0, 2);
204 local_game_state_ = clamped_state;
205 if (game_state_) {
206 *game_state_ = clamped_state;
207 }
208}
209
211 if (!overworld_ || map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
212 return;
213 }
214 auto* map = overworld_->mutable_overworld_map(map_index);
215 if (!map) {
216 return;
217 }
218 map->set_game_state(CurrentGameState(map->game_state()));
219}
220
222 int current_map, bool& show_custom_bg_color_editor) {
223 (void)show_custom_bg_color_editor; // Used by caller for window state
224 if (!overworld_->is_loaded()) {
225 Text(tr("No overworld loaded"));
226 return;
227 }
228
231 Text(tr("Custom background colors require ZSCustomOverworld v2+"));
232 return;
233 }
234
235 Text(tr("Custom Background Color Editor"));
236 Separator();
237
238 // Read enable flag from ROM (not static - must reflect current ROM state)
239 bool use_area_specific_bg_color =
241 if (ImGui::Checkbox(tr("Use Area-Specific Background Color"),
242 &use_area_specific_bg_color)) {
243 // Update ROM data when checkbox is toggled
245 use_area_specific_bg_color ? 0x01 : 0x00;
246 }
247
248 if (use_area_specific_bg_color) {
249 // The edit targets the area parent; display that same persistent value.
250 const int property_map = EffectivePropertyMapId(overworld_, current_map);
251 const auto* map = overworld_->overworld_map(property_map);
252 if (!map) {
253 return;
254 }
255 uint16_t current_color = map->area_specific_bg_color();
256 gfx::SnesColor snes_color(current_color);
257
258 // Convert to ImVec4 for color picker
259 ImVec4 color_vec = gui::ConvertSnesColorToImVec4(snes_color);
260
261 if (ImGui::ColorPicker4(
262 "Background Color", (float*)&color_vec,
263 ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHex)) {
264 // Convert back to SNES color and update
265 gfx::SnesColor new_snes_color = gui::ConvertImVec4ToSnesColor(color_vec);
266 ApplyPropertyEdit({current_map,
268 new_snes_color.snes()});
269 }
270
271 Text(tr("SNES Color: 0x%04X"), current_color);
272 }
273}
274
276 bool& show_overlay_editor) {
277 (void)show_overlay_editor; // Used by caller for window state
278 if (!overworld_->is_loaded()) {
279 Text(tr("No overworld loaded"));
280 return;
281 }
282
284
285 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f),
286 ICON_MD_LAYERS " Visual Effects Configuration");
287 ImGui::Text(tr("Map: 0x%02X"), current_map);
288 Separator();
289
290 if (rom_version == zelda3::OverworldVersion::kVanilla) {
291 ImGui::TextColored(
292 ImVec4(1.0f, 0.8f, 0.4f, 1.0f), ICON_MD_INFO
293 " Enhanced overlay editing requires ZSCustomOverworld v1+");
294 ImGui::Separator();
295 ImGui::TextWrapped(tr(
296 "Subscreen overlays are a vanilla feature used for atmospheric effects "
297 "like fog, rain, and forest canopy. ZSCustomOverworld expands this by "
298 "allowing per-area overlay configuration and additional "
299 "customization."));
300 return;
301 }
302
303 // Help section
304 if (ImGui::CollapsingHeader(ICON_MD_HELP_OUTLINE " What are Visual Effects?",
305 ImGuiTreeNodeFlags_DefaultOpen)) {
306 ImGui::Indent();
307 ImGui::TextWrapped(tr(
308 "Visual effects (subscreen overlays) are semi-transparent layers drawn "
309 "on top of or behind your map. They reference special area maps "
310 "(0x80-0x9F) "
311 "for their tile16 graphics data."));
312 ImGui::Spacing();
313 ImGui::Text(tr("Common uses:"));
314 ImGui::BulletText(tr("Fog effects (Lost Woods, Skull Woods)"));
315 ImGui::BulletText(tr("Rain (Misery Mire)"));
316 ImGui::BulletText(tr("Forest canopy (Lost Woods)"));
317 ImGui::BulletText(tr("Sky backgrounds (Death Mountain)"));
318 ImGui::BulletText(tr("Under bridge views"));
319 ImGui::Unindent();
320 ImGui::Separator();
321 }
322
323 // Read enable flag from ROM (not static - must reflect current ROM state)
324 bool use_subscreen_overlay =
326 if (ImGui::Checkbox(ICON_MD_VISIBILITY " Enable Visual Effect for This Area",
327 &use_subscreen_overlay)) {
328 // Update ROM data when checkbox is toggled
330 use_subscreen_overlay ? 0x01 : 0x00;
331 }
332 if (ImGui::IsItemHovered()) {
333 ImGui::SetTooltip(
334 tr("Enable/disable visual effect overlay for this map area"));
335 }
336
337 if (use_subscreen_overlay) {
338 ImGui::Spacing();
339 uint16_t current_overlay =
340 overworld_->overworld_map(current_map)->subscreen_overlay();
341 if (gui::InputHexWord(ICON_MD_PHOTO " Visual Effect Map ID",
342 &current_overlay, kInputFieldSize + 30)) {
344 0, current_overlay});
345 }
346 if (ImGui::IsItemHovered()) {
347 ImGui::SetTooltip(
348 tr("ID of the special area map (0x80-0x9F) to use for\n"
349 "visual effects. That map's tile16 data will be drawn\n"
350 "as a semi-transparent layer on this area."));
351 }
352
353 // Show description
354 std::string overlay_desc = GetOverlayDescription(current_overlay);
355 ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), ICON_MD_INFO " %s",
356 overlay_desc.c_str());
357
358 ImGui::Separator();
359 if (ImGui::CollapsingHeader(ICON_MD_LIGHTBULB
360 " Common Visual Effect IDs")) {
361 ImGui::Indent();
362 ImGui::BulletText(tr("0x0093 - Triforce Room Curtain"));
363 ImGui::BulletText(tr("0x0094 - Under the Bridge"));
364 ImGui::BulletText(tr("0x0095 - Sky Background (LW Death Mountain)"));
365 ImGui::BulletText(tr("0x0096 - Pyramid Background"));
366 ImGui::BulletText(tr("0x0097 - Fog Overlay (Master Sword Area)"));
367 ImGui::BulletText(tr("0x009C - Lava Background (DW Death Mountain)"));
368 ImGui::BulletText(tr("0x009D - Fog Overlay (Lost/Skull Woods)"));
369 ImGui::BulletText(tr("0x009E - Tree Canopy (Forest)"));
370 ImGui::BulletText(tr("0x009F - Rain Effect (Misery Mire)"));
371 ImGui::BulletText(tr("0x00FF - No Overlay (Disabled)"));
372 ImGui::Unindent();
373 }
374 } else {
375 ImGui::Spacing();
376 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ICON_MD_BLOCK
377 " No visual effects enabled for this area");
378 }
379}
380
382 gui::Canvas& canvas, const OverworldContextTarget& target,
383 bool& current_map_lock, int current_mode, project::YazeProject* project,
384 SharedClipboard* shared_clipboard) {
385 (void)current_mode; // Explicit context actions are independent of tool mode.
386 const int map_id = target.map_id;
387 const bool valid_map = target.valid();
388 canvas.ClearContextMenuItems();
389 // This canvas supplies one complete View menu through editor navigation.
390 canvas.SetShowBuiltinContextMenu(false);
391
392 const bool has_metadata = overworld_ && overworld_->is_loaded() &&
393 valid_map && overworld_->overworld_map(map_id);
394 const auto describe = [&](int id) -> std::string {
395 if (!has_metadata) {
396 return absl::StrFormat("0x%02X", id);
397 }
398 const auto m = BuildOverworldMapMetadata(*overworld_, rom_, project, id,
399 target.game_state);
400 return m.map_name.empty()
401 ? absl::StrFormat("%s | %s", m.map_id_label, m.world_label)
402 : absl::StrFormat("%s | %s | %s", m.map_id_label, m.world_label,
403 m.map_name);
404 };
405 const auto hint = [this](const char* name) -> std::string {
406 return shortcut_hint_ && name ? shortcut_hint_(name) : std::string();
407 };
408 const auto add = [&canvas](gui::CanvasMenuItem item, const char* icon,
409 bool separator_after = false) {
410 item.icon = icon;
411 item.separator_after = separator_after;
412 canvas.AddContextMenuItem(std::move(item));
413 };
414
415 // --- Header: where the click landed -------------------------------------
416 // Menu rows share a label column. Keep metadata in tooltips so long map
417 // names cannot widen every action and its shortcut column.
419 valid_map ? absl::StrFormat("Map 0x%02X", map_id) : "Outside Overworld");
420 if (valid_map) {
421 title.tooltip = describe(map_id);
422 }
423 add(std::move(title), ICON_MD_MAP, !(valid_map && target.tile16_id >= 0));
424 if (valid_map && target.tile16_id >= 0) {
425 const int tile_x =
426 static_cast<int>(target.world_position.x / kTile16Size) % 32;
427 const int tile_y =
428 static_cast<int>(target.world_position.y / kTile16Size) % 32;
430 absl::StrFormat("Tile16 0x%03X", target.tile16_id));
431 tile.tooltip =
432 absl::StrFormat("Tile coordinates: (%d, %d)", tile_x, tile_y);
433 add(std::move(tile), ICON_MD_GRID_ON, true);
434 }
435
436 // --- Tile ------------------------------------------------------------------
437 const bool has_tile = valid_map && target.tile16_id >= 0;
438 auto sample_item = gui::CanvasMenuItem::Conditional(
439 "Sample Tile16",
440 [this, target]() {
441 if (sample_tile16_callback_ && target.valid() && target.tile16_id >= 0)
442 (void)sample_tile16_callback_(target);
443 },
444 [this, has_tile]() { return bool(sample_tile16_callback_) && has_tile; });
445 // I samples the hovered tile; in Brush/Fill a plain right-click does too.
446 sample_item.shortcut = "I";
447 add(std::move(sample_item), ICON_MD_COLORIZE);
449 "Edit Tile16...",
450 [this, target]() {
451 if (edit_tile16_callback_ && target.valid() &&
452 target.tile16_id >= 0)
453 edit_tile16_callback_(target);
454 },
455 [this, has_tile]() {
456 return bool(edit_tile16_callback_) && has_tile;
457 }),
460 "Edit Graphics...",
461 [this, target]() {
462 if (edit_graphics_callback_ && target.valid() &&
463 target.tile16_id >= 0) {
464 edit_graphics_callback_(target);
465 }
466 },
467 [this, has_tile]() {
468 return bool(edit_graphics_callback_) && has_tile;
469 }),
470 ICON_MD_IMAGE, true);
471
472 // --- Map -------------------------------------------------------------------
473 auto select_item = gui::CanvasMenuItem::Conditional(
474 "Select This Map",
475 [this, target]() {
476 if (map_selection_callback_ && target.valid())
477 map_selection_callback_(target.map_id, false);
478 },
479 [this, valid_map]() {
480 return valid_map && bool(map_selection_callback_);
481 });
482 // Hidden while this map's area is already the current one.
483 select_item.visible_condition = [this, target]() {
485 return true;
486 }
487 const int current = current_map_provider_();
488 if (current == target.map_id) {
489 return false;
490 }
491 const auto* current_map = overworld_ && IsValidMapId(current)
492 ? overworld_->overworld_map(current)
493 : nullptr;
494 return !(current_map && current_map->parent() == target.parent_map_id);
495 };
496 add(std::move(select_item), ICON_MD_CHECK);
497
498 auto properties_item = gui::CanvasMenuItem::Conditional(
499 "Map Properties...",
500 [this, target]() {
501 if (!map_selection_callback_ || !target.valid())
502 return;
503 map_selection_callback_(target.map_id, false);
506 },
507 [this, valid_map]() {
508 return valid_map && bool(map_selection_callback_) &&
510 });
511 properties_item.tooltip = "Double-click the map to open properties.";
512 add(std::move(properties_item), ICON_MD_TUNE);
513
514 auto pin_item = gui::CanvasMenuItem::Conditional(
515 "Pin Map",
516 [this, target, &current_map_lock]() {
517 if (!target.valid())
518 return;
519 if (current_map_lock) {
520 current_map_lock = false;
521 } else if (map_selection_callback_) {
522 map_selection_callback_(target.map_id, false);
523 current_map_lock = true;
524 }
525 },
526 [this, valid_map, &current_map_lock]() {
527 return valid_map && (current_map_lock || bool(map_selection_callback_));
528 });
529 pin_item.checked_condition = [&current_map_lock]() {
530 return current_map_lock;
531 };
532 pin_item.shortcut = hint("overworld.toggle_lock");
533 pin_item.tooltip =
534 "Pinned: the current map and its properties stay put while the cursor "
535 "moves. Clicking a map still selects it.";
536
537 // Related maps: the other screens of this area and the other world's
538 // screen at the same position. Selecting one also centers it.
539 gui::CanvasMenuItem related_menu;
540 related_menu.label = "Related Maps";
541 if (has_metadata) {
542 const auto jump = [this](int id) {
543 if (map_jump_callback_) {
545 } else if (map_selection_callback_) {
546 map_selection_callback_(id, false);
547 }
548 };
549 const int world_start = target.world * 0x40;
550 const int world_end =
551 std::min(world_start + 0x40, zelda3::kNumOverworldMaps);
552 for (int id = world_start; id < world_end; ++id) {
553 const auto* member = overworld_->overworld_map(id);
554 if (id == map_id || !member || member->parent() != target.parent_map_id) {
555 continue;
556 }
557 const std::string role =
558 id == target.parent_map_id ? "Area parent: " : "Same area: ";
559 related_menu.subitems.emplace_back(role + describe(id),
560 [jump, id]() { jump(id); });
561 }
562 if (target.world < 2) {
563 const int counterpart = target.world == 0 ? map_id + 0x40 : map_id - 0x40;
564 if (IsValidMapId(counterpart) && overworld_->overworld_map(counterpart)) {
565 if (!related_menu.subitems.empty()) {
566 related_menu.subitems.back().separator_after = true;
567 }
568 related_menu.subitems.emplace_back(
569 std::string("Other world: ") + describe(counterpart),
570 [jump, counterpart]() { jump(counterpart); });
571 }
572 }
573 }
574 const bool has_related = !related_menu.subitems.empty();
575 add(std::move(pin_item), ICON_MD_PUSH_PIN, !has_related);
576 if (has_related) {
577 related_menu.enabled_condition = [this]() {
578 return bool(map_jump_callback_) || bool(map_selection_callback_);
579 };
580 add(std::move(related_menu), ICON_MD_ACCOUNT_TREE, true);
581 }
582
583 // --- Clipboard: area properties (graphics, palettes, music, message) ------
584 if (has_metadata && shared_clipboard) {
586 "Copy Map Properties",
587 [this, target, shared_clipboard]() {
588 shared_clipboard->overworld_map_metadata =
589 CaptureMapMetadataClipboard(*overworld_, target.map_id);
590 shared_clipboard->overworld_map_metadata.scope =
592 shared_clipboard->has_overworld_map_metadata =
593 shared_clipboard->overworld_map_metadata.valid;
594 }),
596 constexpr auto kScope = OverworldMapMetadataClipboardScope::kAll;
597 auto paste_item = gui::CanvasMenuItem::Conditional(
598 "Paste Map Properties",
599 [this, target, shared_clipboard]() {
600 const auto edits = BuildOverworldMetadataPasteEdits(
601 target.map_id, shared_clipboard->overworld_map_metadata, kScope);
602 (void)ApplyPropertyEdits(
603 edits,
604 absl::StrFormat(
605 "Paste map properties from 0x%02X",
606 shared_clipboard->overworld_map_metadata.source_map_id));
607 },
608 [shared_clipboard]() {
609 return shared_clipboard->has_overworld_map_metadata &&
611 shared_clipboard->overworld_map_metadata, kScope);
612 });
613 if (shared_clipboard->has_overworld_map_metadata &&
614 shared_clipboard->overworld_map_metadata.valid) {
616 shared_clipboard->overworld_map_metadata);
617 }
618 add(std::move(paste_item), ICON_MD_CONTENT_PASTE, true);
619 }
620
621 // --- Insert: entities placed at the clicked tile --------------------------
622 gui::CanvasMenuItem insert_menu;
623 insert_menu.label = "Insert";
624 insert_menu.enabled_condition = [this, valid_map]() {
625 return valid_map && bool(entity_insert_callback_);
626 };
627 struct EntityType {
628 const char* icon;
629 const char* label;
630 const char* type;
631 };
632 // Transports have no insert path yet (see docs/internal/gui/context-menus.md).
633 const EntityType entity_types[] = {
634 {ICON_MD_DOOR_FRONT, "Entrance", "entrance"},
635 {ICON_MD_CYCLONE, "Hole", "hole"},
636 {ICON_MD_DOOR_BACK, "Exit", "exit"},
637 {ICON_MD_GRASS, "Item", "item"},
638 {ICON_MD_PEST_CONTROL_RODENT, "Sprite", "sprite"}};
639 // Not a structured binding: lambdas can't capture those before Clang 16.
640 for (const EntityType& entity : entity_types) {
641 const char* type = entity.type;
642 insert_menu.subitems.emplace_back(
643 entity.label, entity.icon, [this, target, type]() {
644 if (entity_insert_callback_ && target.valid())
645 entity_insert_callback_(type, target);
646 });
647 }
648 add(std::move(insert_menu), ICON_MD_ADD_LOCATION);
649
650 // --- View: mirrors the toolbar toggles and zoom buttons -------------------
651 gui::CanvasMenuItem view_menu;
652 view_menu.label = "View";
653 const auto add_toggle = [&](const char* icon, const char* label,
654 const ContextViewToggle& toggle,
655 const char* shortcut_name) {
656 if (!toggle.toggle || !toggle.is_on) {
657 return;
658 }
659 gui::CanvasMenuItem item(label, icon, toggle.toggle);
660 item.checked_condition = toggle.is_on;
661 item.shortcut = hint(shortcut_name);
662 view_menu.subitems.push_back(std::move(item));
663 };
664 add_toggle(ICON_MD_GRID_4X4, "Grid", grid_toggle_, "overworld.toggle_grid");
665 add_toggle(ICON_MD_PLACE, "Entities", entities_toggle_,
666 "overworld.toggle_entities");
667 add_toggle(ICON_MD_LAYERS, "Overlay Preview", overlay_toggle_, nullptr);
668 if (!view_menu.subitems.empty()) {
669 view_menu.subitems.back().separator_after = true;
670 }
671 const auto add_view_action = [&](const char* icon, const char* label,
672 const std::function<void()>& callback,
673 const char* shortcut_name) {
675 label,
676 [callback]() {
677 if (callback)
678 callback();
679 },
680 [callback]() { return bool(callback); });
681 item.icon = icon;
682 item.shortcut = hint(shortcut_name);
683 view_menu.subitems.push_back(std::move(item));
684 };
685 add_view_action(ICON_MD_ZOOM_IN, "Zoom In", zoom_in_callback_,
686 "overworld.zoom_in");
687 add_view_action(ICON_MD_ZOOM_OUT, "Zoom Out", zoom_out_callback_,
688 "overworld.zoom_out");
689 if (zoom_fit_callback_) {
690 add_view_action(ICON_MD_FIT_SCREEN, "Zoom to Fit", zoom_fit_callback_,
691 "overworld.zoom_fit");
692 }
694 add_view_action(ICON_MD_CENTER_FOCUS_STRONG, "Center on Map",
695 center_map_callback_, "overworld.center_map");
696 }
697 add(std::move(view_menu), ICON_MD_VISIBILITY);
698}
699
701 const OverworldPropertyEdit& edit) {
703 return property_edit_callback_(edit);
704 }
705 return ApplyPropertyEditDirect(edit);
706}
707
709 const std::vector<OverworldPropertyEdit>& edits,
710 const std::string& description) {
712 return property_edit_batch_callback_(edits, description);
713 }
714 for (const auto& edit : edits) {
715 if (!CheckPropertyEditSupported(edit).ok()) {
716 continue;
717 }
719 }
720 return absl::OkStatus();
721}
722
724 const OverworldPropertyEdit& edit) const {
725 return CheckFieldSupported(rom_, edit);
726}
727
729 const OverworldPropertyEdit& edit) const {
730 if (!overworld_) {
731 return absl::FailedPreconditionError("Overworld is not available");
732 }
733 const int map_id = EffectivePropertyMapId(
734 const_cast<zelda3::Overworld*>(overworld_), edit.map_id);
735 if (!IsValidMapId(map_id)) {
736 return absl::InvalidArgumentError(
737 absl::StrFormat("Invalid overworld map: %d", edit.map_id));
738 }
739 const auto* map = overworld_->overworld_map(map_id);
740 if (!map) {
741 return absl::NotFoundError(
742 absl::StrFormat("Overworld map 0x%02X is unavailable", map_id));
743 }
744
745 switch (edit.field) {
747 return static_cast<int>(map->area_size());
749 return map->area_graphics();
751 return map->area_palette();
753 return map->main_palette();
755 return map->sprite_graphics(std::clamp(edit.index, 0, 2));
757 return map->sprite_palette(std::clamp(edit.index, 0, 2));
759 return map->animated_gfx();
761 return map->custom_tileset(std::clamp(edit.index, 0, 7));
763 return map->message_id();
765 return map->area_music(std::clamp(edit.index, 0, 3));
767 return *const_cast<zelda3::OverworldMap*>(map)->mutable_mosaic() ? 1 : 0;
769 return map->mosaic_expanded()[std::clamp(edit.index, 0, 3)] ? 1 : 0;
771 return map->area_specific_bg_color();
773 return map->subscreen_overlay();
774 }
775 return absl::InvalidArgumentError("Unknown overworld property field");
776}
777
779 const OverworldPropertyEdit& edit) {
780 if (!overworld_ || !rom_) {
781 return absl::FailedPreconditionError(
782 "Overworld property editing requires loaded overworld data and ROM");
783 }
784 const int map_id = EffectivePropertyMapId(overworld_, edit.map_id);
785 if (!IsValidMapId(map_id)) {
786 return absl::InvalidArgumentError(
787 absl::StrFormat("Invalid overworld map: %d", edit.map_id));
788 }
789 auto* map = overworld_->mutable_overworld_map(map_id);
790 if (!map) {
791 return absl::NotFoundError(
792 absl::StrFormat("Overworld map 0x%02X is unavailable", map_id));
793 }
794
795 const absl::Status supported = CheckFieldSupported(rom_, edit);
796 if (!supported.ok()) {
797 return supported;
798 }
799
800 const int game_state = std::clamp(edit.index, 0, 2);
801 switch (edit.field) {
803 const auto size = static_cast<zelda3::AreaSizeEnum>(edit.value);
804 auto status = overworld_->ConfigureMultiAreaMap(map_id, size);
805 if (!status.ok()) {
806 return status;
807 }
808 RefreshSiblingMapGraphics(map_id, true);
810 break;
811 }
813 map->set_area_graphics(static_cast<uint8_t>(edit.value));
814 RefreshMapProperties(map_id);
815 if (maps_bmp_) {
816 (*maps_bmp_)[map_id].set_modified(true);
817 }
819 map->LoadAreaGraphics();
823 break;
825 map->set_area_palette(static_cast<uint8_t>(edit.value));
826 RefreshMapProperties(map_id);
827 RefreshMapPalette(map_id);
829 break;
831 map->set_main_palette(static_cast<uint8_t>(edit.value));
832 RefreshMapProperties(map_id);
833 RefreshMapPalette(map_id);
835 break;
837 map->set_sprite_graphics(game_state, static_cast<uint8_t>(edit.value));
838 ForceRefreshGraphics(map_id);
839 RefreshMapProperties(map_id);
841 break;
843 map->set_sprite_palette(game_state, static_cast<uint8_t>(edit.value));
844 RefreshMapProperties(map_id);
846 break;
848 map->set_animated_gfx(static_cast<uint8_t>(edit.value));
849 ForceRefreshGraphics(map_id);
850 RefreshMapProperties(map_id);
853 break;
855 const int slot = std::clamp(edit.index, 0, 7);
856 map->set_custom_tileset(slot, static_cast<uint8_t>(edit.value));
858 map->LoadAreaGraphics();
859 ForceRefreshGraphics(map_id);
861 RefreshMapProperties(map_id);
864 break;
865 }
867 map->set_message_id(static_cast<uint16_t>(edit.value));
868 RefreshMapProperties(map_id);
870 break;
872 const int music_state = std::clamp(edit.index, 0, 3);
873 *map->mutable_area_music(music_state) = static_cast<uint8_t>(edit.value);
874 WriteRomByteIfValid(rom_, MusicRomAddressForMapState(map_id, music_state),
875 static_cast<uint8_t>(edit.value));
876 RefreshMapProperties(map_id);
877 break;
878 }
880 *map->mutable_mosaic() = edit.value != 0;
881 RefreshMapProperties(map_id);
883 break;
885 map->set_mosaic_expanded(std::clamp(edit.index, 0, 3), edit.value != 0);
886 RefreshMapProperties(map_id);
888 break;
890 map->set_area_specific_bg_color(static_cast<uint16_t>(edit.value));
891 WriteRomWordIfValid(
893 static_cast<uint16_t>(edit.value));
894 RefreshMapProperties(map_id);
896 break;
898 map->set_subscreen_overlay(static_cast<uint16_t>(edit.value));
899 WriteRomWordIfValid(
901 static_cast<uint16_t>(edit.value));
902 RefreshMapProperties(map_id);
904 break;
905 }
906
907 rom_->set_dirty(true);
908 return absl::OkStatus();
909}
910
916
922
925 return refresh_map_palette_(map_id);
926 }
927 return absl::OkStatus();
928}
929
933 }
934 return absl::OkStatus();
935}
936
939 force_refresh_graphics_(map_index);
940 }
941}
942
944 bool include_self) {
945 if (!overworld_ || !maps_bmp_ || map_index < 0 ||
946 map_index >= zelda3::kNumOverworldMaps) {
947 return;
948 }
949
950 auto* map = overworld_->mutable_overworld_map(map_index);
951 if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
952 return; // No siblings for small areas
953 }
954
955 int parent_id = map->parent();
956 std::vector<int> siblings;
957
958 switch (map->area_size()) {
960 siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
961 break;
963 siblings = {parent_id, parent_id + 1};
964 break;
966 siblings = {parent_id, parent_id + 8};
967 break;
968 default:
969 return;
970 }
971
972 for (int sibling : siblings) {
973 if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) {
974 // Skip self unless include_self is true
975 if (sibling == map_index && !include_self) {
976 continue;
977 }
978
979 // Mark as modified FIRST
980 (*maps_bmp_)[sibling].set_modified(true);
981
982 // Load graphics from ROM
984 overworld_->mutable_overworld_map(sibling)->LoadAreaGraphics();
985
986 // CRITICAL FIX: Force immediate refresh on the sibling
987 // This will trigger the callback to OverworldEditor's
988 // RefreshChildMapOnDemand
989 ForceRefreshGraphics(sibling);
990 }
991 }
992
993 // After marking all siblings, trigger a refresh
994 // This ensures all marked maps get processed
996}
997
998std::string MapPropertiesSystem::GetOverlayDescription(uint16_t overlay_id) {
999 if (overlay_id == 0x0093) {
1000 return "Triforce Room Curtain";
1001 } else if (overlay_id == 0x0094) {
1002 return "Under the Bridge";
1003 } else if (overlay_id == 0x0095) {
1004 return "Sky Background (LW Death Mountain)";
1005 } else if (overlay_id == 0x0096) {
1006 return "Pyramid Background";
1007 } else if (overlay_id == 0x0097) {
1008 return "First Fog Overlay (Master Sword Area)";
1009 } else if (overlay_id == 0x009C) {
1010 return "Lava Background (DW Death Mountain)";
1011 } else if (overlay_id == 0x009D) {
1012 return "Second Fog Overlay (Lost Woods/Skull Woods)";
1013 } else if (overlay_id == 0x009E) {
1014 return "Tree Canopy (Forest)";
1015 } else if (overlay_id == 0x009F) {
1016 return "Rain Effect (Misery Mire)";
1017 } else if (overlay_id == 0x00FF) {
1018 return "No Overlay";
1019 } else {
1020 return "Custom overlay";
1021 }
1022}
1023
1025 int current_world,
1026 bool show_overlay_preview) {
1027 gfx::ScopedTimer timer("map_properties_draw_overlay_preview");
1028
1029 if (!show_overlay_preview || !maps_bmp_ || !canvas_)
1030 return;
1031
1032 // Get subscreen overlay information based on ROM version and map type
1033 uint16_t overlay_id = 0x00FF;
1034 bool has_subscreen_overlay = false;
1035
1036 bool is_special_overworld_map = (current_map >= 0x80 && current_map < 0xA0);
1037
1038 if (is_special_overworld_map) {
1039 // Special overworld maps (0x80-0x9F) do not support subscreen overlays
1040 return;
1041 }
1042
1043 // Light World (0x00-0x3F) and Dark World (0x40-0x7F) maps support subscreen
1044 // overlays for all versions
1045 overlay_id = overworld_->overworld_map(current_map)->subscreen_overlay();
1046 has_subscreen_overlay = (overlay_id != 0x00FF);
1047
1048 if (!has_subscreen_overlay)
1049 return;
1050
1051 // Map subscreen overlay ID to special area map for bitmap
1052 int overlay_map_index = -1;
1053 if (overlay_id >= 0x80 && overlay_id < 0xA0) {
1054 overlay_map_index = overlay_id;
1055 }
1056
1057 if (overlay_map_index < 0 || overlay_map_index >= zelda3::kNumOverworldMaps)
1058 return;
1059
1060 // Get the subscreen overlay map's bitmap
1061 const auto& overlay_bitmap = (*maps_bmp_)[overlay_map_index];
1062 if (!overlay_bitmap.is_active() || !overlay_bitmap.texture())
1063 return;
1064
1065 // Calculate position for subscreen overlay preview on the current map
1066 int current_map_x = current_map % 8;
1067 int current_map_y = current_map / 8;
1068 if (current_world == 1) {
1069 current_map_x = (current_map - 0x40) % 8;
1070 current_map_y = (current_map - 0x40) / 8;
1071 } else if (current_world == 2) {
1072 current_map_x = (current_map - 0x80) % 8;
1073 current_map_y = (current_map - 0x80) / 8;
1074 }
1075
1076 int scale = static_cast<int>(canvas_->global_scale());
1077 int map_x = current_map_x * kOverworldMapSize * scale;
1078 int map_y = current_map_y * kOverworldMapSize * scale;
1079
1080 // Determine if this is a background or foreground subscreen overlay
1081 bool is_background_overlay =
1082 (overlay_id == 0x0095 || overlay_id == 0x0096 || overlay_id == 0x009C);
1083
1084 // Set alpha for semi-transparent preview
1085 ImU32 overlay_color =
1086 is_background_overlay ? IM_COL32(255, 255, 255, 128)
1087 : // Background subscreen overlays - lighter
1088 IM_COL32(255, 255, 255,
1089 180); // Foreground subscreen overlays - more opaque
1090
1091 // Draw the subscreen overlay bitmap with semi-transparency
1092 canvas_->draw_list()->AddImage(
1093 (ImTextureID)(intptr_t)overlay_bitmap.texture(), ImVec2(map_x, map_y),
1094 ImVec2(map_x + kOverworldMapSize * scale,
1095 map_y + kOverworldMapSize * scale),
1096 ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(overlay_color));
1097}
1098
1099} // namespace editor
1100} // 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:49
void set_dirty(bool dirty)
Definition rom.h:189
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
void PrepareMapForGraphicsRefresh(int map_index)
absl::Status ApplyPropertyEdit(const OverworldPropertyEdit &edit)
RefreshMapCallback refresh_map_properties_
RefreshMapPaletteCallback refresh_map_palette_
std::function< void()> center_map_callback_
void DrawOverlayEditor(int current_map, bool &show_overlay_editor)
PropertyEditBatchCallback property_edit_batch_callback_
std::function< void(int, bool)> map_selection_callback_
std::function< std::string(const char *) shortcut_hint_)
std::function< void()> zoom_out_callback_
RefreshPaletteCallback refresh_tile16_blockset_
void DrawOverlayPreviewOnMap(int current_map, int current_world, bool show_overlay_preview)
std::function< bool(const OverworldContextTarget &) sample_tile16_callback_)
absl::Status ApplyPropertyEdits(const std::vector< OverworldPropertyEdit > &edits, const std::string &description={})
absl::Status CheckPropertyEditSupported(const OverworldPropertyEdit &edit) const
std::function< void(int)> map_jump_callback_
std::function< void(const std::string &, const OverworldContextTarget &) entity_insert_callback_)
std::function< void(const OverworldContextTarget &) edit_graphics_callback_)
absl::Status RefreshMapPalette(int map_id)
std::function< void()> zoom_fit_callback_
std::function< void()> zoom_in_callback_
std::function< void(const OverworldContextTarget &) edit_tile16_callback_)
void SetCurrentGameState(int game_state)
void DrawCustomBackgroundColorEditor(int current_map, bool &show_custom_bg_color_editor)
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp_
absl::StatusOr< int > ReadPropertyValue(const OverworldPropertyEdit &edit) const
std::function< void()> open_map_properties_callback_
void SetupCanvasContextMenu(gui::Canvas &canvas, const OverworldContextTarget &target, bool &current_map_lock, int current_mode=0, project::YazeProject *project=nullptr, SharedClipboard *shared_clipboard=nullptr)
absl::Status ApplyPropertyEditDirect(const OverworldPropertyEdit &edit)
void ForceRefreshGraphics(int map_index)
std::function< int()> current_map_provider_
void RefreshSiblingMapGraphics(int map_index, bool include_self=false)
ForceRefreshGraphicsCallback force_refresh_graphics_
std::string GetOverlayDescription(uint16_t overlay_id)
PropertyEditCallback property_edit_callback_
RAII timer for automatic timing management.
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
auto global_scale() const
Definition canvas.h:413
auto draw_list() const
Definition canvas.h:363
void ClearContextMenuItems()
Definition canvas.cc:994
void AddContextMenuItem(const gui::CanvasMenuItem &item)
Definition canvas.cc:971
void SetShowBuiltinContextMenu(bool show)
Definition canvas.h:220
Represents a single Overworld map screen.
const std::array< bool, 4 > & mosaic_expanded() 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 bool SupportsAreaEnum(OverworldVersion version)
Check if ROM supports area enum system (v3+ only)
static bool SupportsAnimatedGFX(OverworldVersion version)
Check if ROM supports animated GFX selection (v3+)
static bool SupportsExpandedSpace(OverworldVersion version)
Check if ROM uses expanded ROM space for overworld data.
auto is_loaded() const
Definition overworld.h:803
auto overworld_map(int i) const
Definition overworld.h:737
auto mutable_overworld_map(int i)
Definition overworld.h:743
absl::Status ConfigureMultiAreaMap(int parent_index, AreaSizeEnum size)
Configure a multi-area map structure (Large/Wide/Tall)
Definition overworld.cc:491
#define ICON_MD_BLOCK
Definition icons.h:269
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_COLORIZE
Definition icons.h:441
#define ICON_MD_CENTER_FOCUS_STRONG
Definition icons.h:386
#define ICON_MD_ACCOUNT_TREE
Definition icons.h:83
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_FIT_SCREEN
Definition icons.h:781
#define ICON_MD_LIGHTBULB
Definition icons.h:1083
#define ICON_MD_PLACE
Definition icons.h:1477
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_ZOOM_OUT
Definition icons.h:2196
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_GRASS
Definition icons.h:891
#define ICON_MD_DOOR_BACK
Definition icons.h:612
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_HELP_OUTLINE
Definition icons.h:935
#define ICON_MD_ADD_LOCATION
Definition icons.h:100
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_GRID_4X4
Definition icons.h:893
#define ICON_MD_PEST_CONTROL_RODENT
Definition icons.h:1430
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
#define ICON_MD_PHOTO
Definition icons.h:1451
#define ICON_MD_CYCLONE
Definition icons.h:514
void WriteRomWordIfValid(Rom *rom, int address, uint16_t value)
int EffectivePropertyMapId(zelda3::Overworld *overworld, int map_id)
absl::Status CheckFieldSupported(const Rom *rom, const OverworldPropertyEdit &edit)
OverworldMapMetadataClipboard CaptureMapMetadataClipboard(const zelda3::Overworld &overworld, int map_id)
bool WriteRomByteIfValid(Rom *rom, int address, uint8_t value)
std::vector< OverworldPropertyEdit > BuildOverworldMetadataPasteEdits(int target_map_id, const OverworldMapMetadataClipboard &clipboard, OverworldMapMetadataClipboardScope requested_scope)
bool CanPasteOverworldMapMetadata(const OverworldMapMetadataClipboard &clipboard, OverworldMapMetadataClipboardScope requested_scope)
constexpr unsigned int kOverworldMapSize
constexpr int kTile16Size
OverworldMapMetadata BuildOverworldMapMetadata(const zelda3::Overworld &overworld, const Rom *rom, const project::YazeProject *project, int map_id, int game_state)
constexpr float kInputFieldSize
Definition entity.cc:28
EntityType
Type of entity that can be selected in the dungeon editor.
std::string DescribeOverworldMapMetadataClipboard(const OverworldMapMetadataClipboard &clipboard)
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:489
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
Definition color.cc:24
gfx::SnesColor ConvertImVec4ToSnesColor(const ImVec4 &color)
Convert standard ImVec4 to SnesColor.
Definition color.cc:37
constexpr int OverworldCustomAreaSpecificBGEnabled
constexpr int kSpecialWorldMapIdStart
constexpr int kNumOverworldMaps
Definition common.h:85
constexpr int kOverworldMusicBeginning
Definition overworld.h:127
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int kOverworldMusicDarkWorld
Definition overworld.h:131
constexpr int kOverworldMusicAgahnim
Definition overworld.h:130
@ kVanilla
0xFF in ROM, no ZScream ASM applied
constexpr int kOverworldMusicMasterSword
Definition overworld.h:129
constexpr int kOverworldMusicZelda
Definition overworld.h:128
constexpr int kDarkWorldMapIdStart
constexpr int OverworldCustomSubscreenOverlayEnabled
constexpr int OverworldCustomAreaSpecificBGPalette
constexpr int OverworldCustomSubscreenOverlayArray
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
SNES color in 15-bit RGB format (BGR555)
A View-menu toggle that mirrors a toolbar toggle.
bool valid(int map_count=zelda3::kNumOverworldMaps) const
OverworldMapMetadataClipboard overworld_map_metadata
Definition editor.h:124
Declarative menu item definition.
Definition canvas_menu.h:64
std::function< bool()> checked_condition
Definition canvas_menu.h:95
std::vector< CanvasMenuItem > subitems
Definition canvas_menu.h:98
std::function< bool()> enabled_condition
Definition canvas_menu.h:85
static CanvasMenuItem Conditional(const std::string &lbl, std::function< void()> cb, std::function< bool()> condition)
std::function< bool()> visible_condition
Definition canvas_menu.h:90
static CanvasMenuItem Disabled(const std::string &lbl)
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
struct snes_color snes_color
SNES color in 15-bit RGB format (BGR555)