yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_editor.cc
Go to the documentation of this file.
1#include "overworld_editor.h"
2
3#ifndef IM_PI
4#define IM_PI 3.14159265358979323846f
5#endif
6
7#include <algorithm>
8#include <cmath>
9#include <filesystem>
10#include <set>
11#include <unordered_map>
12#include <vector>
13
14#include "absl/status/status.h"
15#include "absl/strings/str_format.h"
17#include "app/core/features.h"
21#include "app/gfx/arena.h"
22#include "app/gfx/bitmap.h"
26#include "app/gfx/tilemap.h"
27#include "app/gui/canvas.h"
30#include "app/gui/icons.h"
31#include "app/gui/style.h"
32#include "app/gui/ui_helpers.h"
33#include "app/rom.h"
34#include "app/zelda3/common.h"
37#include "imgui/imgui.h"
38#include "imgui_memory_editor.h"
39#include "util/file_util.h"
40#include "util/hex.h"
41#include "util/log.h"
42#include "util/macro.h"
43
44namespace yaze::editor {
45
46using namespace ImGui;
47
48constexpr float kInputFieldSize = 30.f;
49
51 // Register cards with EditorCardManager
52 auto& card_manager = gui::EditorCardManager::Get();
53
54 card_manager.RegisterCard({
55 .card_id = "overworld.tile16_selector",
56 .display_name = "Tile16 Selector",
57 .icon = ICON_MD_GRID_ON,
58 .category = "Overworld",
59 .shortcut_hint = "Ctrl+Alt+1",
60 .visibility_flag = &show_tile16_selector_,
61 .priority = 10
62 });
63
64 card_manager.RegisterCard({
65 .card_id = "overworld.tile8_selector",
66 .display_name = "Tile8 Selector",
67 .icon = ICON_MD_GRID_3X3,
68 .category = "Overworld",
69 .shortcut_hint = "Ctrl+Alt+2",
70 .visibility_flag = &show_tile8_selector_,
71 .priority = 20
72 });
73
74 card_manager.RegisterCard({
75 .card_id = "overworld.area_graphics",
76 .display_name = "Area Graphics",
77 .icon = ICON_MD_IMAGE,
78 .category = "Overworld",
79 .shortcut_hint = "Ctrl+Alt+3",
80 .visibility_flag = &show_area_gfx_,
81 .priority = 30
82 });
83
84 card_manager.RegisterCard({
85 .card_id = "overworld.scratch",
86 .display_name = "Scratch Workspace",
87 .icon = ICON_MD_DRAW,
88 .category = "Overworld",
89 .shortcut_hint = "Ctrl+Alt+4",
90 .visibility_flag = &show_scratch_,
91 .priority = 40
92 });
93
94 card_manager.RegisterCard({
95 .card_id = "overworld.gfx_groups",
96 .display_name = "GFX Groups",
97 .icon = ICON_MD_FOLDER,
98 .category = "Overworld",
99 .shortcut_hint = "Ctrl+Alt+5",
100 .visibility_flag = &show_gfx_groups_,
101 .priority = 50
102 });
103
104 card_manager.RegisterCard({
105 .card_id = "overworld.usage_stats",
106 .display_name = "Usage Statistics",
107 .icon = ICON_MD_ANALYTICS,
108 .category = "Overworld",
109 .shortcut_hint = "Ctrl+Alt+6",
110 .visibility_flag = &show_usage_stats_,
111 .priority = 60
112 });
113
114 card_manager.RegisterCard({
115 .card_id = "overworld.v3_settings",
116 .display_name = "v3 Settings",
117 .icon = ICON_MD_SETTINGS,
118 .category = "Overworld",
119 .shortcut_hint = "Ctrl+Alt+7",
120 .visibility_flag = &show_v3_settings_,
121 .priority = 70
122 });
123
124 // Original initialization code below:
125 // Initialize MapPropertiesSystem with canvas and bitmap data
126 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
128
129 // Set up refresh callbacks for MapPropertiesSystem
130 map_properties_system_->SetRefreshCallbacks(
131 [this]() { this->RefreshMapProperties(); },
132 [this]() { this->RefreshOverworldMap(); },
133 [this]() -> absl::Status { return this->RefreshMapPalette(); },
134 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
135 [this](int map_index) { this->ForceRefreshGraphics(map_index); }
136 );
137
138 // Initialize OverworldEntityRenderer for entity visualization
139 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
141
142 // Setup Canvas Automation API callbacks (Phase 4)
144
145 // Note: Context menu is now setup dynamically in DrawOverworldCanvas()
146 // for context-aware menu items based on current map state
147
148 // Old toolset initialization removed - using modern CompactToolbar instead
149}
150
151absl::Status OverworldEditor::Load() {
152 gfx::ScopedTimer timer("OverworldEditor::Load");
153
154 LOG_DEBUG("OverworldEditor", "Loading overworld.");
155 if (!rom_ || !rom_->is_loaded()) {
156 return absl::FailedPreconditionError("ROM not loaded");
157 }
158
163
164 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
167
168 // Set up callback for when tile16 changes are committed
169 tile16_editor_.set_on_changes_committed([this]() -> absl::Status {
170 // Regenerate the overworld editor's tile16 blockset
172
173 // Force refresh of the current overworld map to show changes
175
176 LOG_DEBUG("OverworldEditor", "Overworld editor refreshed after Tile16 changes");
177 return absl::OkStatus();
178 });
179
181 all_gfx_loaded_ = true;
182 return absl::OkStatus();
183}
184
186 status_ = absl::OkStatus();
187
188 // Process deferred textures for smooth loading
190
193 return status_;
194 }
195
196 // Modern layout - no tabs, just toolbar + canvas + floating cards
197 DrawToolset();
199
200 // Create session-aware cards (non-static for multi-session support)
201 gui::EditorCard tile16_card(MakeCardTitle("Tile16 Selector").c_str(), ICON_MD_GRID_3X3);
202 gui::EditorCard tile8_card(MakeCardTitle("Tile8 Selector").c_str(), ICON_MD_GRID_4X4);
203 gui::EditorCard area_gfx_card(MakeCardTitle("Area Graphics").c_str(), ICON_MD_IMAGE);
204 gui::EditorCard scratch_card(MakeCardTitle("Scratch Space").c_str(), ICON_MD_BRUSH);
205 gui::EditorCard tile16_editor_card(MakeCardTitle("Tile16 Editor").c_str(), ICON_MD_GRID_ON);
206 gui::EditorCard gfx_groups_card(MakeCardTitle("Graphics Groups").c_str(), ICON_MD_COLLECTIONS);
207 gui::EditorCard usage_stats_card(MakeCardTitle("Usage Statistics").c_str(), ICON_MD_ANALYTICS);
208 gui::EditorCard v3_settings_card(MakeCardTitle("v3 Settings").c_str(), ICON_MD_TUNE);
209
210 // Configure card positions (these settings persist via imgui.ini)
211 static bool cards_configured = false;
212 if (!cards_configured) {
213 // Position cards for optimal workflow
214 tile16_card.SetDefaultSize(300, 600);
216
217 tile8_card.SetDefaultSize(280, 500);
219
220 area_gfx_card.SetDefaultSize(300, 400);
222
223 scratch_card.SetDefaultSize(350, 500);
225
226 tile16_editor_card.SetDefaultSize(800, 600);
228
229 gfx_groups_card.SetDefaultSize(700, 550);
231
232 usage_stats_card.SetDefaultSize(600, 500);
234
235 v3_settings_card.SetDefaultSize(500, 600);
237
238 cards_configured = true;
239 }
240
241 // Main canvas (full width when cards are docked)
243
244 // Floating tile selector cards (4 tabs converted to separate cards)
246 if (tile16_card.Begin(&show_tile16_selector_)) {
248 }
249 tile16_card.End(); // ALWAYS call End after Begin
250 }
251
253 if (tile8_card.Begin(&show_tile8_selector_)) {
255 gui::BeginChildWithScrollbar("##Tile8SelectorScrollRegion");
257 ImGui::EndChild();
259 }
260 tile8_card.End(); // ALWAYS call End after Begin
261 }
262
263 if (show_area_gfx_) {
264 if (area_gfx_card.Begin(&show_area_gfx_)) {
266 }
267 area_gfx_card.End(); // ALWAYS call End after Begin
268 }
269
270 if (show_scratch_) {
271 if (scratch_card.Begin(&show_scratch_)) {
273 }
274 scratch_card.End(); // ALWAYS call End after Begin
275 }
276
277 // Tile16 Editor popup-only (no tab)
279 if (tile16_editor_card.Begin(&show_tile16_editor_)) {
280 if (rom_->is_loaded()) {
282 } else {
283 gui::CenterText("No ROM loaded");
284 }
285 }
286 tile16_editor_card.End(); // ALWAYS call End after Begin
287 }
288
289 // Graphics Groups popup
290 if (show_gfx_groups_) {
291 if (gfx_groups_card.Begin(&show_gfx_groups_)) {
292 if (rom_->is_loaded()) {
294 } else {
295 gui::CenterText("No ROM loaded");
296 }
297 }
298 gfx_groups_card.End(); // ALWAYS call End after Begin
299 }
300
301 // Usage Statistics popup
302 if (show_usage_stats_) {
303 if (usage_stats_card.Begin(&show_usage_stats_)) {
304 if (rom_->is_loaded()) {
306 } else {
307 gui::CenterText("No ROM loaded");
308 }
309 }
310 usage_stats_card.End(); // ALWAYS call End after Begin
311 }
312
313 // Area Configuration Panel (detailed editing)
315 ImGui::SetNextWindowSize(ImVec2(650, 750), ImGuiCond_FirstUseEver);
316 if (ImGui::Begin(ICON_MD_TUNE " Area Configuration###AreaConfig", &show_map_properties_panel_)) {
319 }
320 }
321 ImGui::End();
322 }
323
324 // Custom Background Color Editor
326 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
327 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color", &show_custom_bg_color_editor_)) {
329 map_properties_system_->DrawCustomBackgroundColorEditor(current_map_, show_custom_bg_color_editor_);
330 }
331 }
332 ImGui::End();
333 }
334
335 // Visual Effects Editor (Subscreen Overlays)
337 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
338 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor", &show_overlay_editor_)) {
341 }
342 }
343 ImGui::End();
344 }
345
346 // --- BEGIN CENTRALIZED INTERACTION LOGIC ---
347 auto* hovered_entity = entity_renderer_->hovered_entity();
348
349 // Handle all MOUSE mode interactions here
351 // --- CONTEXT MENUS & POPOVERS ---
352 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
353 if (hovered_entity) {
354 current_entity_ = hovered_entity;
355 switch (hovered_entity->entity_type_) {
357 current_exit_ = *static_cast<zelda3::OverworldExit*>(hovered_entity);
358 ImGui::OpenPopup("Exit editor");
359 break;
362 *static_cast<zelda3::OverworldEntrance*>(hovered_entity);
363 ImGui::OpenPopup("Entrance Editor");
364 break;
366 current_item_ = *static_cast<zelda3::OverworldItem*>(hovered_entity);
367 ImGui::OpenPopup("Item editor");
368 break;
370 current_sprite_ = *static_cast<zelda3::Sprite*>(hovered_entity);
371 ImGui::OpenPopup("Sprite editor");
372 break;
373 default:
374 break;
375 }
376 }
377 }
378
379 // --- DOUBLE-CLICK ACTIONS ---
380 if (hovered_entity && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
381 if (hovered_entity->entity_type_ == zelda3::GameEntity::EntityType::kExit) {
383 static_cast<zelda3::OverworldExit*>(hovered_entity)->room_id_;
384 } else if (hovered_entity->entity_type_ ==
387 static_cast<zelda3::OverworldEntrance*>(hovered_entity)->entrance_id_;
388 }
389 }
390 }
391
392 // --- DRAW POPUPS ---
394 if (current_entity_ &&
397 rom_->set_dirty(true);
398 }
399 }
405 rom_->set_dirty(true);
406 }
407 }
409 if (current_entity_ &&
412 rom_->set_dirty(true);
413 }
414 }
416 if (current_entity_ &&
419 rom_->set_dirty(true);
420 }
421 }
422 // --- END CENTRALIZED LOGIC ---
423
424 return status_;
425}
426
428 static bool use_work_area = true;
429 static ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
430 ImGuiWindowFlags_NoMove |
431 ImGuiWindowFlags_NoSavedSettings;
432 const ImGuiViewport* viewport = ImGui::GetMainViewport();
433 ImGui::SetNextWindowPos(use_work_area ? viewport->WorkPos : viewport->Pos);
434 ImGui::SetNextWindowSize(use_work_area ? viewport->WorkSize : viewport->Size);
435 if (ImGui::Begin("Fullscreen Overworld Editor", &overworld_canvas_fullscreen_,
436 flags)) {
437 // Draws the toolset for editing the Overworld.
438 DrawToolset();
440 }
441 ImGui::End();
442}
443
445 // Modern adaptive toolbar with inline mode switching and properties
446 static gui::Toolset toolbar;
447
448 // IMPORTANT: Don't make asm_version static - it needs to update after ROM upgrade
449 uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
450
451 // Don't use WidgetIdScope here - it conflicts with ImGui::Begin/End ID stack in cards
452 // Widgets register themselves individually instead
453
454 toolbar.Begin();
455
456 // Mode buttons - simplified to 2 modes
457 toolbar.BeginModeGroup();
458
459 if (toolbar.ModeButton(ICON_MD_MOUSE, current_mode == EditingMode::MOUSE, "Mouse Mode (1)\nNavigate, pan, and manage entities")) {
461 }
462
463 if (toolbar.ModeButton(ICON_MD_DRAW, current_mode == EditingMode::DRAW_TILE, "Tile Paint Mode (2)\nDraw tiles on the map")) {
465 }
466
467 toolbar.EndModeGroup();
468
469 // Entity editing indicator (shows current entity mode if active)
471 toolbar.AddSeparator();
472 const char* entity_label = "";
473 const char* entity_icon = "";
474 switch (entity_edit_mode_) {
475 case EntityEditMode::ENTRANCES: entity_icon = ICON_MD_DOOR_FRONT; entity_label = "Entrances"; break;
476 case EntityEditMode::EXITS: entity_icon = ICON_MD_DOOR_BACK; entity_label = "Exits"; break;
477 case EntityEditMode::ITEMS: entity_icon = ICON_MD_GRASS; entity_label = "Items"; break;
478 case EntityEditMode::SPRITES: entity_icon = ICON_MD_PEST_CONTROL_RODENT; entity_label = "Sprites"; break;
479 case EntityEditMode::TRANSPORTS: entity_icon = ICON_MD_ADD_LOCATION; entity_label = "Transports"; break;
480 case EntityEditMode::MUSIC: entity_icon = ICON_MD_MUSIC_NOTE; entity_label = "Music"; break;
481 default: break;
482 }
483 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s Editing: %s", entity_icon, entity_label);
484 }
485
486 // ROM version badge (already read above)
487 toolbar.AddRomBadge(asm_version, [this]() {
488 ImGui::OpenPopup("UpgradeROMVersion");
489 });
490
491 // Inline map properties with icon labels - use toolbar methods for consistency
492 if (toolbar.AddProperty(ICON_MD_IMAGE, " Gfx",
493 overworld_.mutable_overworld_map(current_map_)->mutable_area_graphics(),
494 [this]() {
495 // CORRECT ORDER: Properties first, then graphics reload
496
497 // 1. Propagate properties to siblings FIRST (this also calls LoadAreaGraphics on siblings)
498 RefreshMapProperties();
499
500 // 2. Force immediate refresh of current map and all siblings
501 maps_bmp_[current_map_].set_modified(true);
502 RefreshChildMapOnDemand(current_map_);
503 RefreshSiblingMapGraphics(current_map_);
504
505 // 3. Update tile selector
506 RefreshTile16Blockset();
507 })) {
508 // Property changed
509 }
510
511 if (toolbar.AddProperty(ICON_MD_PALETTE, " Pal",
512 overworld_.mutable_overworld_map(current_map_)->mutable_area_palette(),
513 [this]() {
514 // Palette changes also need to propagate to siblings
515 RefreshSiblingMapGraphics(current_map_);
516 RefreshMapProperties();
517 status_ = RefreshMapPalette();
518 RefreshOverworldMap();
519 })) {
520 // Property changed
521 }
522
523 toolbar.AddSeparator();
524
525 // Quick actions
526 if (toolbar.AddAction(ICON_MD_ZOOM_OUT, "Zoom Out")) {
528 }
529
530 if (toolbar.AddAction(ICON_MD_ZOOM_IN, "Zoom In")) {
532 }
533
534 if (toolbar.AddToggle(ICON_MD_OPEN_IN_FULL, &overworld_canvas_fullscreen_, "Fullscreen (F11)")) {
535 // Toggled by helper
536 }
537
538 toolbar.AddSeparator();
539
540 // Card visibility toggles (with automation-friendly paths)
541 if (toolbar.AddAction(ICON_MD_GRID_3X3, "Toggle Tile16 Selector")) {
543 }
544
545 if (toolbar.AddAction(ICON_MD_GRID_4X4, "Toggle Tile8 Selector")) {
547 }
548
549 if (toolbar.AddAction(ICON_MD_IMAGE, "Toggle Area Graphics")) {
551 }
552
553 if (toolbar.AddAction(ICON_MD_BRUSH, "Toggle Scratch Space")) {
555 }
556
557 toolbar.AddSeparator();
558
559 if (toolbar.AddAction(ICON_MD_GRID_VIEW, "Open Tile16 Editor")) {
561 }
562
563 if (toolbar.AddAction(ICON_MD_COLLECTIONS, "Open Graphics Groups")) {
565 }
566
567 if (toolbar.AddUsageStatsButton("Open Usage Statistics")) {
569 }
570
571 if (toolbar.AddAction(ICON_MD_TUNE, "Open Area Configuration")) {
573 }
574
575 toolbar.End();
576
577 // ROM Upgrade Popup (rendered outside toolbar to avoid ID conflicts)
578 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
579 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
580 ImGui::Separator();
581 ImGui::TextWrapped(
582 "This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
583 "enabling advanced features like custom tile graphics, animated GFX,\n"
584 "wide/tall areas, and more.");
585 ImGui::Separator();
586
587 uint8_t current_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
588 ImGui::Text("Current Version: %s",
589 current_version == 0xFF ? "Vanilla" : absl::StrFormat("v%d", current_version).c_str());
590
591 static int target_version = 3;
592 ImGui::RadioButton("v2 (Basic features)", &target_version, 2);
593 ImGui::SameLine();
594 ImGui::RadioButton("v3 (All features)", &target_version, 3);
595
596 ImGui::Separator();
597
598 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150, 0))) {
599 auto status = ApplyZSCustomOverworldASM(target_version);
600 if (status.ok()) {
601 // CRITICAL: Reload the editor to reflect changes
602 status_ = Clear();
603 status_ = Load();
604 ImGui::CloseCurrentPopup();
605 } else {
606 LOG_ERROR("OverworldEditor", "Upgrade failed: %s", status.message().data());
607 }
608 }
609 ImGui::SameLine();
610 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150, 0))) {
611 ImGui::CloseCurrentPopup();
612 }
613
614 ImGui::EndPopup();
615 }
616
617 // All editor windows are now rendered in Update() using either EditorCard system
618 // or MapPropertiesSystem for map-specific panels. This keeps the toolset clean
619 // and prevents ImGui ID stack issues.
620
621 // Legacy window code removed - windows rendered in Update() include:
622 // - Graphics Groups (EditorCard)
623 // - Area Configuration (MapPropertiesSystem)
624 // - Background Color Editor (MapPropertiesSystem)
625 // - Visual Effects Editor (MapPropertiesSystem)
626 // - Tile16 Editor, Usage Stats, etc. (EditorCards)
627
628 // Keyboard shortcuts for the Overworld Editor
629 if (!ImGui::IsAnyItemActive()) {
630 using enum EditingMode;
631
632 // Tool shortcuts (simplified)
633 if (ImGui::IsKeyDown(ImGuiKey_1)) {
635 } else if (ImGui::IsKeyDown(ImGuiKey_2)) {
637 }
638
639 // Entity editing shortcuts (3-8)
640 if (ImGui::IsKeyDown(ImGuiKey_3)) {
643 } else if (ImGui::IsKeyDown(ImGuiKey_4)) {
646 } else if (ImGui::IsKeyDown(ImGuiKey_5)) {
649 } else if (ImGui::IsKeyDown(ImGuiKey_6)) {
652 } else if (ImGui::IsKeyDown(ImGuiKey_7)) {
655 } else if (ImGui::IsKeyDown(ImGuiKey_8)) {
658 }
659
660 // View shortcuts
661 if (ImGui::IsKeyDown(ImGuiKey_F11)) {
663 }
664
665 // Toggle map lock with L key
666 if (ImGui::IsKeyDown(ImGuiKey_L) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
668 }
669
670 // Toggle Tile16 editor with T key
671 if (ImGui::IsKeyDown(ImGuiKey_T) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
673 }
674 }
675}
676
678 int xx = 0;
679 int yy = 0;
680 for (int i = 0; i < 0x40; i++) {
681 int world_index = i + (current_world_ * 0x40);
682
683 // Bounds checking to prevent crashes
684 if (world_index < 0 || world_index >= static_cast<int>(maps_bmp_.size())) {
685 continue; // Skip invalid map index
686 }
687
688 // Don't apply scale to coordinates - scale is applied to canvas rendering
689 int map_x = xx * kOverworldMapSize;
690 int map_y = yy * kOverworldMapSize;
691
692 // Check if the map has a texture, if not, ensure it gets loaded
693 if (!maps_bmp_[world_index].texture() &&
694 maps_bmp_[world_index].is_active()) {
695 EnsureMapTexture(world_index);
696 }
697
698 // Only draw if the map has a texture or is the currently selected map
699 if (maps_bmp_[world_index].texture() || world_index == current_map_) {
700 // Draw without applying scale here - canvas handles zoom uniformly
701 ow_map_canvas_.DrawBitmap(maps_bmp_[world_index], map_x, map_y, 1.0f);
702 } else {
703 // Draw a placeholder for maps that haven't loaded yet
704 ImDrawList* draw_list = ImGui::GetWindowDrawList();
705 ImVec2 canvas_pos = ow_map_canvas_.zero_point();
706 ImVec2 placeholder_pos =
707 ImVec2(canvas_pos.x + map_x, canvas_pos.y + map_y);
708 ImVec2 placeholder_size =
710
711 // Modern loading indicator with theme colors
712 draw_list->AddRectFilled(
713 placeholder_pos,
714 ImVec2(placeholder_pos.x + placeholder_size.x,
715 placeholder_pos.y + placeholder_size.y),
716 IM_COL32(32, 32, 32, 128)); // Dark gray with transparency
717
718 // Animated loading spinner
719 ImVec2 spinner_pos = ImVec2(
720 placeholder_pos.x + placeholder_size.x / 2,
721 placeholder_pos.y + placeholder_size.y / 2
722 );
723
724 const float spinner_radius = 8.0f;
725 const float rotation = static_cast<float>(ImGui::GetTime()) * 3.0f;
726 const float start_angle = rotation;
727 const float end_angle = rotation + IM_PI * 1.5f;
728
729 draw_list->PathArcTo(spinner_pos, spinner_radius, start_angle, end_angle, 12);
730 draw_list->PathStroke(IM_COL32(100, 180, 100, 255), 0, 2.5f);
731 }
732
733 xx++;
734 if (xx >= 8) {
735 yy++;
736 xx = 0;
737 }
738 }
739}
740
742 // Determine which overworld map the user is currently editing.
743 auto mouse_position = ow_map_canvas_.drawn_tile_position();
744
745 int map_x = mouse_position.x / kOverworldMapSize;
746 int map_y = mouse_position.y / kOverworldMapSize;
747 current_map_ = map_x + map_y * 8;
748 if (current_world_ == 1) {
749 current_map_ += 0x40;
750 } else if (current_world_ == 2) {
751 current_map_ += 0x80;
752 }
753
754 // Bounds checking to prevent crashes
755 if (current_map_ < 0 || current_map_ >= static_cast<int>(maps_bmp_.size())) {
756 return; // Invalid map index, skip drawing
757 }
758
759 // Validate tile16_blockset_ before calling GetTilemapData
761 tile16_blockset_.atlas.vector().empty()) {
762 LOG_ERROR("OverworldEditor",
763 "Error: tile16_blockset_ is not properly initialized (active: %s, "
764 "size: %zu)",
765 tile16_blockset_.atlas.is_active() ? "true" : "false",
766 tile16_blockset_.atlas.vector().size());
767 return; // Skip drawing if blockset is invalid
768 }
769
770 // Render the updated map bitmap.
772 RenderUpdatedMapBitmap(mouse_position, tile_data);
773
774 // Calculate the correct superX and superY values
775 int superY = current_map_ / 8;
776 int superX = current_map_ % 8;
777 int mouse_x = mouse_position.x;
778 int mouse_y = mouse_position.y;
779 // Calculate the correct tile16_x and tile16_y positions
780 int tile16_x = (mouse_x % kOverworldMapSize) / (kOverworldMapSize / 32);
781 int tile16_y = (mouse_y % kOverworldMapSize) / (kOverworldMapSize / 32);
782
783 // Update the overworld_.map_tiles() based on tile16 ID and current world
784 auto& selected_world =
785 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
786 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
787 : overworld_.mutable_map_tiles()->special_world;
788
789 int index_x = superX * 32 + tile16_x;
790 int index_y = superY * 32 + tile16_y;
791
792 selected_world[index_x][index_y] = current_tile16_;
793}
794
796 const ImVec2& click_position, const std::vector<uint8_t>& tile_data) {
797
798 // Bounds checking to prevent crashes
799 if (current_map_ < 0 || current_map_ >= static_cast<int>(maps_bmp_.size())) {
800 LOG_ERROR("OverworldEditor",
801 "ERROR: RenderUpdatedMapBitmap - Invalid current_map_ %d "
802 "(maps_bmp_.size()=%zu)",
803 current_map_, maps_bmp_.size());
804 return; // Invalid map index, skip rendering
805 }
806
807 // Calculate the tile index for x and y based on the click_position
808 int tile_index_x =
809 (static_cast<int>(click_position.x) % kOverworldMapSize) / kTile16Size;
810 int tile_index_y =
811 (static_cast<int>(click_position.y) % kOverworldMapSize) / kTile16Size;
812
813 // Calculate the pixel start position based on tile index and tile size
814 ImVec2 start_position;
815 start_position.x = static_cast<float>(tile_index_x * kTile16Size);
816 start_position.y = static_cast<float>(tile_index_y * kTile16Size);
817
818 // Update the bitmap's pixel data based on the start_position and tile_data
819 gfx::Bitmap& current_bitmap = maps_bmp_[current_map_];
820
821 // Validate bitmap state before writing
822 if (!current_bitmap.is_active() || current_bitmap.size() == 0) {
823 LOG_ERROR("OverworldEditor",
824 "ERROR: RenderUpdatedMapBitmap - Bitmap %d is not active or has no "
825 "data (active=%s, size=%zu)",
826 current_map_, current_bitmap.is_active() ? "true" : "false",
827 current_bitmap.size());
828 return;
829 }
830
831 for (int y = 0; y < kTile16Size; ++y) {
832 for (int x = 0; x < kTile16Size; ++x) {
833 int pixel_index =
834 (start_position.y + y) * kOverworldMapSize + (start_position.x + x);
835
836 // Bounds check for pixel index
837 if (pixel_index < 0 ||
838 pixel_index >= static_cast<int>(current_bitmap.size())) {
839 LOG_ERROR("OverworldEditor",
840 "ERROR: RenderUpdatedMapBitmap - pixel_index %d out of bounds "
841 "(bitmap size=%zu)",
842 pixel_index, current_bitmap.size());
843 continue;
844 }
845
846 // Bounds check for tile data
847 int tile_data_index = y * kTile16Size + x;
848 if (tile_data_index < 0 ||
849 tile_data_index >= static_cast<int>(tile_data.size())) {
850 LOG_ERROR("OverworldEditor",
851 "ERROR: RenderUpdatedMapBitmap - tile_data_index %d out of bounds "
852 "(tile_data size=%zu)",
853 tile_data_index, tile_data.size());
854 continue;
855 }
856
857 current_bitmap.WriteToPixel(pixel_index, tile_data[tile_data_index]);
858 }
859 }
860
861 current_bitmap.set_modified(true);
862
863 // Immediately update the texture to reflect changes
864 // TODO: Queue texture for later rendering.
865 // core::Renderer::Get().UpdateBitmap(&current_bitmap);
866}
867
869 LOG_DEBUG("OverworldEditor", "CheckForOverworldEdits: Frame %d",
870 ImGui::GetFrameCount());
871
873
874 // User has selected a tile they want to draw from the blockset
875 // and clicked on the canvas.
876 // Note: With TileSelectorWidget, we check if a valid tile is selected instead of canvas points
877 if (current_tile16_ >= 0 &&
881 }
882
884 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
885 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
886 LOG_DEBUG("OverworldEditor", "CheckForOverworldEdits: About to apply rectangle selection");
887
888 auto& selected_world =
889 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
890 : (current_world_ == 1)
891 ? overworld_.mutable_map_tiles()->dark_world
892 : overworld_.mutable_map_tiles()->special_world;
893 // new_start_pos and new_end_pos
894 auto start = ow_map_canvas_.selected_points()[0];
895 auto end = ow_map_canvas_.selected_points()[1];
896
897 // Calculate the bounds of the rectangle in terms of 16x16 tile indices
898 int start_x = std::floor(start.x / kTile16Size) * kTile16Size;
899 int start_y = std::floor(start.y / kTile16Size) * kTile16Size;
900 int end_x = std::floor(end.x / kTile16Size) * kTile16Size;
901 int end_y = std::floor(end.y / kTile16Size) * kTile16Size;
902
903 if (start_x > end_x)
904 std::swap(start_x, end_x);
905 if (start_y > end_y)
906 std::swap(start_y, end_y);
907
908 constexpr int local_map_size = 512; // Size of each local map
909 // Number of tiles per local map (since each tile is 16x16)
910 constexpr int tiles_per_local_map = local_map_size / kTile16Size;
911
912 LOG_DEBUG("OverworldEditor",
913 "CheckForOverworldEdits: About to fill rectangle with "
914 "current_tile16_=%d",
916
917 // Apply the selected tiles to each position in the rectangle
918 // CRITICAL FIX: Use pre-computed tile16_ids_ instead of recalculating from selected_tiles_
919 // This prevents wrapping issues when dragging near boundaries
920 int i = 0;
921 for (int y = start_y; y <= end_y && i < static_cast<int>(selected_tile16_ids_.size()); y += kTile16Size) {
922 for (int x = start_x; x <= end_x && i < static_cast<int>(selected_tile16_ids_.size()); x += kTile16Size, ++i) {
923
924 // Determine which local map (512x512) the tile is in
925 int local_map_x = x / local_map_size;
926 int local_map_y = y / local_map_size;
927
928 // Calculate the tile's position within its local map
929 int tile16_x = (x % local_map_size) / kTile16Size;
930 int tile16_y = (y % local_map_size) / kTile16Size;
931
932 // Calculate the index within the overall map structure
933 int index_x = local_map_x * tiles_per_local_map + tile16_x;
934 int index_y = local_map_y * tiles_per_local_map + tile16_y;
935
936 // FIXED: Use pre-computed tile ID from the ORIGINAL selection
937 int tile16_id = selected_tile16_ids_[i];
938 // Bounds check for the selected world array, accounting for rectangle size
939 // Ensure the entire rectangle fits within the world bounds
940 int rect_width = ((end_x - start_x) / kTile16Size) + 1;
941 int rect_height = ((end_y - start_y) / kTile16Size) + 1;
942
943 // Prevent painting from wrapping around at the edges of large maps
944 // Only allow painting if the entire rectangle is within the same 512x512 local map
945 int start_local_map_x = start_x / local_map_size;
946 int start_local_map_y = start_y / local_map_size;
947 int end_local_map_x = end_x / local_map_size;
948 int end_local_map_y = end_y / local_map_size;
949
950 bool in_same_local_map = (start_local_map_x == end_local_map_x) && (start_local_map_y == end_local_map_y);
951
952 if (in_same_local_map &&
953 index_x >= 0 && (index_x + rect_width - 1) < 0x200 &&
954 index_y >= 0 && (index_y + rect_height - 1) < 0x200) {
955 selected_world[index_x][index_y] = tile16_id;
956
957 // CRITICAL FIX: Also update the bitmap directly like single tile drawing
958 ImVec2 tile_position(x, y);
959 auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile16_id);
960 if (!tile_data.empty()) {
961 RenderUpdatedMapBitmap(tile_position, tile_data);
962 LOG_DEBUG("OverworldEditor",
963 "CheckForOverworldEdits: Updated bitmap at position (%d,%d) "
964 "with tile16_id=%d",
965 x, y, tile16_id);
966 } else {
967 LOG_ERROR("OverworldEditor", "ERROR: Failed to get tile data for tile16_id=%d",
968 tile16_id);
969 }
970 }
971 }
972 }
973
975 // Clear the rectangle selection after applying
976 // This is commented out for now, will come back to later.
977 // ow_map_canvas_.mutable_selected_tiles()->clear();
978 // ow_map_canvas_.mutable_points()->clear();
979 LOG_DEBUG("OverworldEditor",
980 "CheckForOverworldEdits: Rectangle selection applied and cleared");
981 }
982 }
983}
984
987
988 // Single tile case
989 if (ow_map_canvas_.selected_tile_pos().x != -1) {
993
994 // Scroll blockset canvas to show the selected tile
996 }
997
998 // Rectangle selection case - use member variable instead of static local
1000 // Get the tile16 IDs from the selected tile ID positions
1001 selected_tile16_ids_.clear();
1002
1003 if (ow_map_canvas_.selected_tiles().size() > 0) {
1004 // Set the current world and map in overworld for proper tile lookup
1007 for (auto& each : ow_map_canvas_.selected_tiles()) {
1009 }
1010 }
1011 }
1012 // Create a composite image of all the tile16s selected
1015}
1016
1018 if (!context_)
1019 return absl::FailedPreconditionError("No editor context");
1020 // If a rectangle selection exists, copy its tile16 IDs into shared clipboard
1022 !ow_map_canvas_.selected_points().empty()) {
1023 std::vector<int> ids;
1024 const auto start = ow_map_canvas_.selected_points()[0];
1025 const auto end = ow_map_canvas_.selected_points()[1];
1026 const int start_x =
1027 static_cast<int>(std::floor(std::min(start.x, end.x) / 16.0f));
1028 const int end_x =
1029 static_cast<int>(std::floor(std::max(start.x, end.x) / 16.0f));
1030 const int start_y =
1031 static_cast<int>(std::floor(std::min(start.y, end.y) / 16.0f));
1032 const int end_y =
1033 static_cast<int>(std::floor(std::max(start.y, end.y) / 16.0f));
1034 const int width = end_x - start_x + 1;
1035 const int height = end_y - start_y + 1;
1036 ids.reserve(width * height);
1039 for (int y = start_y; y <= end_y; ++y) {
1040 for (int x = start_x; x <= end_x; ++x) {
1041 ids.push_back(overworld_.GetTile(x, y));
1042 }
1043 }
1044
1049 return absl::OkStatus();
1050 }
1051 // Single tile copy fallback
1052 if (current_tile16_ >= 0) {
1057 return absl::OkStatus();
1058 }
1059 return absl::FailedPreconditionError("Nothing selected to copy");
1060}
1061
1063 if (!context_)
1064 return absl::FailedPreconditionError("No editor context");
1066 return absl::FailedPreconditionError("Clipboard empty");
1067 }
1068 if (ow_map_canvas_.points().empty() &&
1070 return absl::FailedPreconditionError("No paste target");
1071 }
1072
1073 // Determine paste anchor position (use current mouse drawn tile position)
1074 const ImVec2 anchor = ow_map_canvas_.drawn_tile_position();
1075
1076 // Compute anchor in tile16 grid within the current map
1077 const int tile16_x =
1078 (static_cast<int>(anchor.x) % kOverworldMapSize) / kTile16Size;
1079 const int tile16_y =
1080 (static_cast<int>(anchor.y) % kOverworldMapSize) / kTile16Size;
1081
1082 auto& selected_world =
1083 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
1084 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
1085 : overworld_.mutable_map_tiles()->special_world;
1086
1087 const int superY = current_map_ / 8;
1088 const int superX = current_map_ % 8;
1089 const int tiles_per_local_map = 512 / kTile16Size;
1090
1091 const int width = context_->shared_clipboard.overworld_width;
1092 const int height = context_->shared_clipboard.overworld_height;
1094
1095 // Guard
1096 if (width * height != static_cast<int>(ids.size())) {
1097 return absl::InternalError("Clipboard dimensions mismatch");
1098 }
1099
1100 for (int dy = 0; dy < height; ++dy) {
1101 for (int dx = 0; dx < width; ++dx) {
1102 const int id = ids[dy * width + dx];
1103 const int gx = tile16_x + dx;
1104 const int gy = tile16_y + dy;
1105
1106 const int global_x = superX * 32 + gx;
1107 const int global_y = superY * 32 + gy;
1108 if (global_x < 0 || global_x >= 256 || global_y < 0 || global_y >= 256)
1109 continue;
1110 selected_world[global_x][global_y] = id;
1111 }
1112 }
1113
1115 return absl::OkStatus();
1116}
1117
1119 // 4096x4096, 512x512 maps and some are larges maps 1024x1024
1120 // CRITICAL FIX: Use canvas hover position (not raw ImGui mouse) for proper coordinate sync
1121 // hover_mouse_pos() already returns canvas-local coordinates (world space, not screen space)
1122 const auto mouse_position = ow_map_canvas_.hover_mouse_pos();
1123 const int large_map_size = 1024;
1124
1125 // Calculate which small map the mouse is currently over
1126 // No need to subtract canvas_zero_point - mouse_position is already in world coordinates
1127 int map_x = mouse_position.x / kOverworldMapSize;
1128 int map_y = mouse_position.y / kOverworldMapSize;
1129
1130 // Calculate the index of the map in the `maps_bmp_` vector
1131 int hovered_map = map_x + map_y * 8;
1132 if (current_world_ == 1) {
1133 hovered_map += 0x40;
1134 } else if (current_world_ == 2) {
1135 hovered_map += 0x80;
1136 }
1137
1138 // Only update current_map_ if not locked
1139 if (!current_map_lock_) {
1140 current_map_ = hovered_map;
1142
1143 // Ensure the current map is built (on-demand loading)
1145 }
1146
1147 const int current_highlighted_map = current_map_;
1148
1149 // Check if ZSCustomOverworld v3 is present
1150 uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
1151 bool use_v3_area_sizes = (asm_version >= 3);
1152
1153 // Get area size for v3+ ROMs, otherwise use legacy logic
1154 if (use_v3_area_sizes) {
1156 auto area_size = overworld_.overworld_map(current_map_)->area_size();
1157 const int highlight_parent =
1158 overworld_.overworld_map(current_highlighted_map)->parent();
1159
1160 // Calculate parent map coordinates accounting for world offset
1161 int parent_map_x;
1162 int parent_map_y;
1163 if (current_world_ == 0) {
1164 // Light World (0x00-0x3F)
1165 parent_map_x = highlight_parent % 8;
1166 parent_map_y = highlight_parent / 8;
1167 } else if (current_world_ == 1) {
1168 // Dark World (0x40-0x7F)
1169 parent_map_x = (highlight_parent - 0x40) % 8;
1170 parent_map_y = (highlight_parent - 0x40) / 8;
1171 } else {
1172 // Special World (0x80-0x9F)
1173 parent_map_x = (highlight_parent - 0x80) % 8;
1174 parent_map_y = (highlight_parent - 0x80) / 8;
1175 }
1176
1177 // Draw outline based on area size
1178 switch (area_size) {
1179 case AreaSizeEnum::LargeArea:
1180 // 2x2 grid (1024x1024)
1182 parent_map_y * kOverworldMapSize,
1183 large_map_size, large_map_size);
1184 break;
1185 case AreaSizeEnum::WideArea:
1186 // 2x1 grid (1024x512) - horizontal
1188 parent_map_y * kOverworldMapSize,
1189 large_map_size, kOverworldMapSize);
1190 break;
1191 case AreaSizeEnum::TallArea:
1192 // 1x2 grid (512x1024) - vertical
1194 parent_map_y * kOverworldMapSize,
1195 kOverworldMapSize, large_map_size);
1196 break;
1197 case AreaSizeEnum::SmallArea:
1198 default:
1200 parent_map_y * kOverworldMapSize,
1202 break;
1203 }
1204 } else {
1205 // Legacy logic for vanilla and v2 ROMs
1206 int world_offset = current_world_ * 0x40;
1207 if (overworld_.overworld_map(current_map_)->is_large_map() ||
1208 overworld_.overworld_map(current_map_)->large_index() != 0) {
1209 const int highlight_parent =
1210 overworld_.overworld_map(current_highlighted_map)->parent();
1211
1212 // CRITICAL FIX: Account for world offset when calculating parent coordinates
1213 // For Dark World (0x40-0x7F), parent IDs are in range 0x40-0x7F
1214 // For Special World (0x80-0x9F), parent IDs are in range 0x80-0x9F
1215 // We need to subtract the world offset to get display grid coordinates (0-7)
1216 int parent_map_x;
1217 int parent_map_y;
1218 if (current_world_ == 0) {
1219 // Light World (0x00-0x3F)
1220 parent_map_x = highlight_parent % 8;
1221 parent_map_y = highlight_parent / 8;
1222 } else if (current_world_ == 1) {
1223 // Dark World (0x40-0x7F) - subtract 0x40 to get display coordinates
1224 parent_map_x = (highlight_parent - 0x40) % 8;
1225 parent_map_y = (highlight_parent - 0x40) / 8;
1226 } else {
1227 // Special World (0x80-0x9F) - subtract 0x80 to get display coordinates
1228 parent_map_x = (highlight_parent - 0x80) % 8;
1229 parent_map_y = (highlight_parent - 0x80) / 8;
1230 }
1231
1233 parent_map_y * kOverworldMapSize,
1234 large_map_size, large_map_size);
1235 } else {
1236 // Calculate map coordinates accounting for world offset
1237 int current_map_x;
1238 int current_map_y;
1239 if (current_world_ == 0) {
1240 // Light World (0x00-0x3F)
1241 current_map_x = current_highlighted_map % 8;
1242 current_map_y = current_highlighted_map / 8;
1243 } else if (current_world_ == 1) {
1244 // Dark World (0x40-0x7F)
1245 current_map_x = (current_highlighted_map - 0x40) % 8;
1246 current_map_y = (current_highlighted_map - 0x40) / 8;
1247 } else {
1248 // Special World (0x80-0x9F) - use display coordinates based on current_world_
1249 // The special world maps are displayed in the same 8x8 grid as LW/DW
1250 current_map_x = (current_highlighted_map - 0x80) % 8;
1251 current_map_y = (current_highlighted_map - 0x80) / 8;
1252 }
1254 current_map_y * kOverworldMapSize,
1256 }
1257 }
1258
1259 // Ensure current map has texture created for rendering
1261
1262 if (maps_bmp_[current_map_].modified()) {
1265
1266 // Ensure tile16 blockset is fully updated before rendering
1268 // TODO: Queue texture for later rendering.
1269 // Renderer::Get().UpdateBitmap(&tile16_blockset_.atlas);
1270 }
1271
1272 // Update map texture with the traditional direct update approach
1273 // TODO: Queue texture for later rendering.
1274 // Renderer::Get().UpdateBitmap(&maps_bmp_[current_map_]);
1275 maps_bmp_[current_map_].set_modified(false);
1276 }
1277
1278 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
1280 }
1281
1282 // If double clicked, toggle the current map
1283 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
1285 }
1286
1287 return absl::OkStatus();
1288}
1289
1290// Overworld Canvas Pan/Zoom Helpers
1291
1292namespace {
1293
1294// Calculate the total canvas content size based on world layout
1296 // 8x8 grid of 512x512 maps = 4096x4096 total
1297 constexpr float kWorldSize = 512.0f * 8.0f; // 4096
1298 return ImVec2(kWorldSize * scale, kWorldSize * scale);
1299}
1300
1301// Clamp scroll position to valid bounds
1302ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size) {
1303 // Calculate maximum scroll values
1304 float max_scroll_x = std::max(0.0f, content_size.x - visible_size.x);
1305 float max_scroll_y = std::max(0.0f, content_size.y - visible_size.y);
1306
1307 // Clamp to valid range [min_scroll, 0]
1308 // Note: Canvas uses negative scrolling for right/down
1309 float clamped_x = std::clamp(scroll.x, -max_scroll_x, 0.0f);
1310 float clamped_y = std::clamp(scroll.y, -max_scroll_y, 0.0f);
1311
1312 return ImVec2(clamped_x, clamped_y);
1313}
1314
1315} // namespace
1316
1318 // Middle mouse button panning (works in all modes)
1319 if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle) && ImGui::IsItemHovered()) {
1321
1322 // Get mouse delta and apply to scroll
1323 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
1324 ImVec2 current_scroll = ow_map_canvas_.scrolling();
1325 ImVec2 new_scroll = ImVec2(
1326 current_scroll.x + mouse_delta.x,
1327 current_scroll.y + mouse_delta.y
1328 );
1329
1330 // Clamp scroll to boundaries
1331 ImVec2 content_size = CalculateOverworldContentSize(ow_map_canvas_.global_scale());
1332 ImVec2 visible_size = ow_map_canvas_.canvas_size();
1333 new_scroll = ClampScrollPosition(new_scroll, content_size, visible_size);
1334
1335 ow_map_canvas_.set_scrolling(new_scroll);
1336 }
1337
1338 if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle) && middle_mouse_dragging_) {
1339 middle_mouse_dragging_ = false;
1340 }
1341
1342
1343}
1344
1346 if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
1347 return;
1348 }
1349
1350 const ImGuiIO& io = ImGui::GetIO();
1351
1352 // Mouse wheel zoom with Ctrl key
1353 if (io.MouseWheel != 0.0f && io.KeyCtrl) {
1354 float current_scale = ow_map_canvas_.global_scale();
1355 float zoom_delta = io.MouseWheel * 0.1f;
1356 float new_scale = current_scale + zoom_delta;
1357
1358 // Clamp zoom range (0.25x to 2.0x)
1359 new_scale = std::clamp(new_scale, 0.25f, 2.0f);
1360
1361 if (new_scale != current_scale) {
1362 // Get mouse position relative to canvas
1363 ImVec2 mouse_pos_canvas = ImVec2(
1364 io.MousePos.x - ow_map_canvas_.zero_point().x,
1365 io.MousePos.y - ow_map_canvas_.zero_point().y
1366 );
1367
1368 // Calculate content position under mouse before zoom
1369 ImVec2 scroll = ow_map_canvas_.scrolling();
1370 ImVec2 content_pos_before = ImVec2(
1371 (mouse_pos_canvas.x - scroll.x) / current_scale,
1372 (mouse_pos_canvas.y - scroll.y) / current_scale
1373 );
1374
1375 // Apply new scale
1377
1378 // Calculate new scroll to keep same content under mouse
1379 ImVec2 new_scroll = ImVec2(
1380 mouse_pos_canvas.x - (content_pos_before.x * new_scale),
1381 mouse_pos_canvas.y - (content_pos_before.y * new_scale)
1382 );
1383
1384 // Clamp scroll to boundaries with new scale
1385 ImVec2 content_size = CalculateOverworldContentSize(new_scale);
1386 ImVec2 visible_size = ow_map_canvas_.canvas_size();
1387 new_scroll = ClampScrollPosition(new_scroll, content_size, visible_size);
1388
1389 ow_map_canvas_.set_scrolling(new_scroll);
1390 }
1391 }
1392}
1393
1398
1400 float scale = ow_map_canvas_.global_scale();
1401 ImVec2 content_size = CalculateOverworldContentSize(scale);
1402 ImVec2 visible_size = ow_map_canvas_.canvas_size();
1403
1404 // Center the view
1405 ImVec2 centered_scroll = ImVec2(
1406 -(content_size.x - visible_size.x) / 2.0f,
1407 -(content_size.y - visible_size.y) / 2.0f
1408 );
1409
1410 ow_map_canvas_.set_scrolling(centered_scroll);
1411}
1412
1414 // Legacy wrapper - now calls HandleOverworldPan
1416}
1417
1419 // Simplified map settings - compact row with popup panels for detailed editing
1421 map_properties_system_->DrawSimplifiedMapSettings(
1425 game_state_, (int&)current_mode);
1426 }
1427
1431
1432 // Setup dynamic context menu based on current map state (Phase 3B)
1434 map_properties_system_->SetupCanvasContextMenu(
1438 }
1439
1440 // Handle pan and zoom (works in all modes)
1443
1444 // Context menu only in MOUSE mode
1446 if (entity_renderer_->hovered_entity() == nullptr) {
1448 }
1449 } else if (current_mode == EditingMode::DRAW_TILE) {
1450 // Tile painting mode - handle tile edits and right-click tile picking
1452 }
1453
1454 if (overworld_.is_loaded()) {
1456
1457 // Draw all entities using the entity renderer
1458 // Convert entity_edit_mode_ to legacy mode int for entity renderer
1459 int entity_mode_int = static_cast<int>(entity_edit_mode_);
1461 current_world_, entity_mode_int);
1463 current_world_, entity_mode_int);
1464 entity_renderer_->DrawItems(current_world_, entity_mode_int);
1465 entity_renderer_->DrawSprites(current_world_, game_state_, entity_mode_int);
1466
1467 // Draw overlay preview if enabled
1469 map_properties_system_->DrawOverlayPreviewOnMap(
1471 }
1472
1475 }
1476 // CRITICAL FIX: Use canvas hover state, not ImGui::IsItemHovered()
1477 // IsItemHovered() checks the LAST drawn item, which could be entities/overlay,
1478 // not the canvas InvisibleButton. ow_map_canvas_.IsMouseHovering() correctly
1479 // tracks whether mouse is over the canvas area.
1482
1483 // --- BEGIN NEW DRAG/DROP LOGIC ---
1485 auto hovered_entity = entity_renderer_->hovered_entity();
1486
1487 // 1. Initiate drag
1488 if (!is_dragging_entity_ && hovered_entity &&
1489 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1490 dragged_entity_ = hovered_entity;
1491 is_dragging_entity_ = true;
1495 }
1496 }
1497
1498 // 2. Update drag
1500 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
1501 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1502 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
1503 float scale = ow_map_canvas_.global_scale();
1504 if (scale > 0.0f) {
1505 dragged_entity_->x_ += mouse_delta.x / scale;
1506 dragged_entity_->y_ += mouse_delta.y / scale;
1507 }
1508 }
1509
1510 // 3. End drag
1511 if (is_dragging_entity_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
1512 if (dragged_entity_) {
1517 rom_->set_dirty(true);
1518 }
1519 is_dragging_entity_ = false;
1520 dragged_entity_ = nullptr;
1522 }
1523 }
1524 // --- END NEW DRAG/DROP LOGIC ---
1525 }
1526
1529 EndChild();
1530 gui::EndNoPadding(); // End the no-padding style that was started at line 1254
1531}
1532
1535 ImGui::BeginGroup();
1536 gui::BeginChildWithScrollbar("##Tile16SelectorScrollRegion");
1538
1539 if (!blockset_selector_) {
1540 gui::TileSelectorWidget::Config selector_config;
1541 selector_config.tile_size = 16;
1542 selector_config.display_scale = 2.0f;
1543 selector_config.tiles_per_row = 8;
1544 selector_config.total_tiles = zelda3::kNumTile16Individual;
1545 selector_config.draw_offset = ImVec2(2.0f, 0.0f);
1546 selector_config.highlight_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f);
1547
1548 blockset_selector_ = std::make_unique<gui::TileSelectorWidget>(
1549 "OwBlocksetSelector", selector_config);
1550 blockset_selector_->AttachCanvas(&blockset_canvas_);
1551 }
1552
1554
1556 bool atlas_ready = map_blockset_loaded_ && atlas.is_active();
1557 auto result = blockset_selector_->Render(atlas, atlas_ready);
1558
1559 if (result.selection_changed) {
1560 current_tile16_ = result.selected_tile;
1562 if (!status.ok()) {
1563 // Store error but ensure we close the child before returning
1564 EndChild();
1565 ImGui::EndGroup();
1566 return status;
1567 }
1568 // Note: We do NOT auto-scroll here because it breaks user interaction.
1569 // The canvas should only scroll when explicitly requested (e.g., when
1570 // selecting a tile from the overworld canvas via ScrollBlocksetCanvasToCurrentTile).
1571 }
1572
1573 if (result.tile_double_clicked) {
1574 show_tile16_editor_ = true;
1575 }
1576
1577 EndChild();
1578 ImGui::EndGroup();
1579 return absl::OkStatus();
1580}
1581
1585 if (all_gfx_loaded_) {
1586 int key = 0;
1587 for (auto& value : gfx::Arena::Get().gfx_sheets()) {
1588 int offset = 0x40 * (key + 1);
1589 int top_left_y = graphics_bin_canvas_.zero_point().y + 2;
1590 if (key >= 1) {
1591 top_left_y = graphics_bin_canvas_.zero_point().y + 0x40 * key;
1592 }
1593 auto texture = value.texture();
1594 graphics_bin_canvas_.draw_list()->AddImage(
1595 (ImTextureID)(intptr_t)texture,
1596 ImVec2(graphics_bin_canvas_.zero_point().x + 2, top_left_y),
1597 ImVec2(graphics_bin_canvas_.zero_point().x + 0x100,
1598 graphics_bin_canvas_.zero_point().y + offset));
1599 key++;
1600 }
1601 }
1604}
1605
1607 if (overworld_.is_loaded()) {
1608 // Always ensure current map graphics are loaded
1609 if (!current_graphics_set_.contains(current_map_)) {
1612 gfx::Bitmap bmp;
1613 // TODO: Queue texture for later rendering.
1614 // Renderer::Get().CreateAndRenderBitmap(0x80, kOverworldMapSize, 0x08,
1615 // overworld_.current_graphics(), bmp,
1616 // palette_);
1618 }
1619 }
1620
1622 ImGui::BeginGroup();
1623 gui::BeginChildWithScrollbar("##AreaGraphicsScrollRegion");
1626 {
1628 if (current_graphics_set_.contains(current_map_) &&
1629 current_graphics_set_[current_map_].is_active()) {
1631 2.0f);
1632 }
1636 }
1637 EndChild();
1638 ImGui::EndGroup();
1639 return absl::OkStatus();
1640}
1641
1642// DrawTileSelector() removed - replaced by individual card system in Update()
1643// DrawOverworldEntrances(), DrawOverworldExits(), DrawOverworldItems(), DrawOverworldSprites()
1644// removed - moved to OverworldEntityRenderer
1645
1647 if (core::FeatureFlags::get().overworld.kSaveOverworldMaps) {
1652 }
1653 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1655 }
1656 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1658 }
1659 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1661 }
1662 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1665 }
1666 return absl::OkStatus();
1667}
1668
1670 gfx::ScopedTimer timer("LoadGraphics");
1671
1672 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1673 // Load the Link to the Past overworld.
1674 {
1675 gfx::ScopedTimer load_timer("Overworld::Load");
1677 }
1679
1680 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1681
1682 // Phase 1: Create bitmaps without textures for faster loading
1683 // This avoids blocking the main thread with GPU texture creation
1684 {
1685 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1686 // TODO: Queue texture for later rendering.
1687 // Renderer::Get().CreateBitmapWithoutTexture(0x80, kOverworldMapSize, 0x40,
1688 // overworld_.current_graphics(),
1689 // current_gfx_bmp_, palette_);
1690 }
1691
1692 LOG_DEBUG("OverworldEditor", "Loading overworld tileset (deferred textures).");
1693 {
1694 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1695 // TODO: Queue texture for later rendering.
1696 // Renderer::Get().CreateBitmapWithoutTexture(
1697 // 0x80, 0x2000, 0x08, overworld_.tile16_blockset_data(),
1698 // tile16_blockset_bmp_, palette_);
1699 }
1700 map_blockset_loaded_ = true;
1701
1702 // Copy the tile16 data into individual tiles.
1703 auto tile16_blockset_data = overworld_.tile16_blockset_data();
1704 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
1705
1706 {
1707 gfx::ScopedTimer tilemap_timer("CreateTilemap");
1709 gfx::CreateTilemap(renderer_,tile16_blockset_data, 0x80, 0x2000, kTile16Size,
1711
1712 // Queue texture creation for the tile16 blockset atlas
1716 }
1717 }
1718
1719 // Phase 2: Create bitmaps only for essential maps initially
1720 // Non-essential maps will be created on-demand when accessed
1721 constexpr int kEssentialMapsPerWorld = 8;
1722 constexpr int kLightWorldEssential = kEssentialMapsPerWorld;
1723 constexpr int kDarkWorldEssential =
1724 zelda3::kDarkWorldMapIdStart + kEssentialMapsPerWorld;
1725 constexpr int kSpecialWorldEssential =
1726 zelda3::kSpecialWorldMapIdStart + kEssentialMapsPerWorld;
1727
1728 LOG_DEBUG("OverworldEditor",
1729 "Creating bitmaps for essential maps only (first %d maps per world)",
1730 kEssentialMapsPerWorld);
1731
1732 std::vector<gfx::Bitmap*> maps_to_texture;
1733 maps_to_texture.reserve(kEssentialMapsPerWorld *
1734 3); // 8 maps per world * 3 worlds
1735
1736 {
1737 gfx::ScopedTimer maps_timer("CreateEssentialOverworldMaps");
1738 for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
1739 bool is_essential = false;
1740
1741 // Check if this is an essential map
1742 if (i < kLightWorldEssential) {
1743 is_essential = true;
1744 } else if (i >= zelda3::kDarkWorldMapIdStart && i < kDarkWorldEssential) {
1745 is_essential = true;
1746 } else if (i >= zelda3::kSpecialWorldMapIdStart &&
1747 i < kSpecialWorldEssential) {
1748 is_essential = true;
1749 }
1750
1751 if (is_essential) {
1753 auto palette = overworld_.current_area_palette();
1754 try {
1755 // Create bitmap data and surface but defer texture creation
1758 maps_bmp_[i].SetPalette(palette);
1759 maps_to_texture.push_back(&maps_bmp_[i]);
1760 } catch (const std::bad_alloc& e) {
1761 std::cout << "Error allocating map " << i << ": " << e.what()
1762 << std::endl;
1763 continue;
1764 }
1765 }
1766 // Non-essential maps will be created on-demand when accessed
1767 }
1768 }
1769
1770 // Phase 3: Create textures only for currently visible maps
1771 // Only create textures for the first few maps initially
1772 const int initial_texture_count =
1773 std::min(4, static_cast<int>(maps_to_texture.size()));
1774 {
1775 gfx::ScopedTimer initial_textures_timer("CreateInitialTextures");
1776 for (int i = 0; i < initial_texture_count; ++i) {
1777 // Queue texture creation/update for initial maps via Arena's deferred system
1779 gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]);
1780 }
1781 }
1782
1783 // Queue remaining maps for progressive loading via Arena
1784 // Priority based on current world (0 = current world, 11+ = other worlds)
1785 for (size_t i = initial_texture_count; i < maps_to_texture.size(); ++i) {
1786 // Determine priority based on which world this map belongs to
1787 int map_index = -1;
1788 for (int j = 0; j < zelda3::kNumOverworldMaps; ++j) {
1789 if (&maps_bmp_[j] == maps_to_texture[i]) {
1790 map_index = j;
1791 break;
1792 }
1793 }
1794
1795 int priority = 15; // Default low priority
1796 if (map_index >= 0) {
1797 int map_world = map_index / 0x40;
1798 priority = (map_world == current_world_) ? 5 : 15; // Current world = priority 5, others = 15
1799 }
1800
1801 // Queue texture creation for remaining maps via Arena's deferred system
1802 // Note: Priority system to be implemented in future enhancement
1804 gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]);
1805 }
1806
1807 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
1808 {
1809 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
1811 }
1812 }
1813
1814 return absl::OkStatus();
1815}
1816
1818 // Render the sprites for each Overworld map
1819 const int depth = 0x10;
1820 for (int i = 0; i < 3; i++)
1821 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
1822 int width = sprite.width();
1823 int height = sprite.height();
1824 if (width == 0 || height == 0) {
1825 continue;
1826 }
1827 if (sprite_previews_.size() < sprite.id()) {
1828 sprite_previews_.resize(sprite.id() + 1);
1829 }
1830 sprite_previews_[sprite.id()].Create(width, height, depth,
1831 *sprite.preview_graphics());
1832 sprite_previews_[sprite.id()].SetPalette(palette_);
1833 // TODO: Queue texture for later rendering.
1834 // Renderer::Get().RenderBitmap(&(sprite_previews_[sprite.id()]));
1835 }
1836 return absl::OkStatus();
1837}
1838
1840 // Process queued texture commands via Arena's deferred system
1841 if (renderer_) {
1843 }
1844
1845 // Also process deferred map refreshes for modified maps
1846 int refresh_count = 0;
1847 const int max_refreshes_per_frame = 2;
1848
1849 for (int i = 0; i < zelda3::kNumOverworldMaps && refresh_count < max_refreshes_per_frame; ++i) {
1850 if (maps_bmp_[i].modified() && maps_bmp_[i].is_active()) {
1851 // Check if this map is in current world (prioritize)
1852 bool is_current_world = (i / 0x40 == current_world_);
1853 bool is_current_map = (i == current_map_);
1854
1855 if (is_current_map || is_current_world) {
1857 refresh_count++;
1858 }
1859 }
1860 }
1861}
1862
1864 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
1865 return;
1866 }
1867
1868 // Ensure the map is built first (on-demand loading)
1869 auto status = overworld_.EnsureMapBuilt(map_index);
1870 if (!status.ok()) {
1871 LOG_ERROR("OverworldEditor", "Failed to build map %d: %s", map_index,
1872 status.message());
1873 return;
1874 }
1875
1876 auto& bitmap = maps_bmp_[map_index];
1877
1878 // If bitmap doesn't exist yet (non-essential map), create it now
1879 if (!bitmap.is_active()) {
1880 overworld_.set_current_map(map_index);
1881 auto palette = overworld_.current_area_palette();
1882 try {
1883 bitmap.Create(kOverworldMapSize, kOverworldMapSize, 0x80,
1885 bitmap.SetPalette(palette);
1886 } catch (const std::bad_alloc& e) {
1887 LOG_ERROR("OverworldEditor", "Error allocating bitmap for map %d: %s",
1888 map_index, e.what());
1889 return;
1890 }
1891 }
1892
1893 if (!bitmap.texture() && bitmap.is_active()) {
1894 // Queue texture creation for this map
1897 }
1898}
1899
1901 overworld_.mutable_overworld_map(map_index)->LoadAreaGraphics();
1902 status_ = overworld_.mutable_overworld_map(map_index)->BuildTileset();
1904 status_ = overworld_.mutable_overworld_map(map_index)->BuildTiles16Gfx(
1907 status_ = overworld_.mutable_overworld_map(map_index)->BuildBitmap(
1909 maps_bmp_[map_index].set_data(
1910 overworld_.mutable_overworld_map(map_index)->bitmap_data());
1911 maps_bmp_[map_index].set_modified(true);
1913}
1914
1916 // Use the new on-demand refresh system
1918}
1919
1928 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
1929 return;
1930 }
1931
1932 // Check if the map is actually visible or being edited
1933 bool is_current_map = (map_index == current_map_);
1934 bool is_current_world = (map_index / 0x40 == current_world_);
1935
1936 // For non-current maps in non-current worlds, defer the refresh
1937 if (!is_current_map && !is_current_world) {
1938 // Mark for deferred refresh - will be processed when the map becomes visible
1939 maps_bmp_[map_index].set_modified(true);
1940 return;
1941 }
1942
1943 // For visible maps, do immediate refresh
1944 RefreshChildMapOnDemand(map_index);
1945}
1946
1951 auto* map = overworld_.mutable_overworld_map(map_index);
1952
1953 // Check what actually needs to be refreshed
1954 bool needs_graphics_rebuild = maps_bmp_[map_index].modified();
1955 bool needs_palette_rebuild = false; // Could be tracked more granularly
1956
1957 if (needs_graphics_rebuild) {
1958 // Only rebuild what's actually changed
1959 map->LoadAreaGraphics();
1960
1961 // Rebuild tileset only if graphics changed
1962 auto status = map->BuildTileset();
1963 if (!status.ok()) {
1964 LOG_ERROR("OverworldEditor", "Failed to build tileset for map %d: %s",
1965 map_index, status.message().data());
1966 return;
1967 }
1968
1969 // Rebuild tiles16 graphics
1970 status = map->BuildTiles16Gfx(*overworld_.mutable_tiles16(),
1971 overworld_.tiles16().size());
1972 if (!status.ok()) {
1973 LOG_ERROR("OverworldEditor", "Failed to build tiles16 graphics for map %d: %s",
1974 map_index, status.message().data());
1975 return;
1976 }
1977
1978 // Rebuild bitmap
1979 status = map->BuildBitmap(overworld_.GetMapTiles(current_world_));
1980 if (!status.ok()) {
1981 LOG_ERROR("OverworldEditor", "Failed to build bitmap for map %d: %s",
1982 map_index, status.message().data());
1983 return;
1984 }
1985
1986 // Update bitmap data
1987 maps_bmp_[map_index].set_data(map->bitmap_data());
1988 maps_bmp_[map_index].set_modified(false);
1989
1990 // Validate surface synchronization to help debug crashes
1991 if (!maps_bmp_[map_index].ValidateDataSurfaceSync()) {
1992 LOG_WARN("OverworldEditor", "Warning: Surface synchronization issue detected for map %d",
1993 map_index);
1994 }
1995
1996 // Queue texture update to ensure changes are visible
1997 if (maps_bmp_[map_index].texture()) {
2000 } else {
2003 }
2004 }
2005
2006 // Handle multi-area maps (large, wide, tall) with safe coordination
2007 // Check if ZSCustomOverworld v3 is present
2008 uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
2009 bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF);
2010
2011 if (use_v3_area_sizes) {
2012 // Use v3 multi-area coordination
2013 RefreshMultiAreaMapsSafely(map_index, map);
2014 } else {
2015 // Legacy logic: only handle large maps for vanilla/v2
2016 if (map->is_large_map()) {
2017 RefreshMultiAreaMapsSafely(map_index, map);
2018 }
2019 }
2020}
2021
2030 zelda3::OverworldMap* map) {
2032
2033 // Skip if this is already a processed sibling to avoid double-processing
2034 static std::set<int> currently_processing;
2035 if (currently_processing.count(map_index)) {
2036 return;
2037 }
2038
2039 auto area_size = map->area_size();
2040 if (area_size == AreaSizeEnum::SmallArea) {
2041 return; // No siblings to coordinate
2042 }
2043
2044 LOG_DEBUG("OverworldEditor",
2045 "RefreshMultiAreaMapsSafely: Processing %s area map %d (parent: %d)",
2046 (area_size == AreaSizeEnum::LargeArea) ? "large"
2047 : (area_size == AreaSizeEnum::WideArea) ? "wide"
2048 : "tall",
2049 map_index, map->parent());
2050
2051 // Determine all maps that are part of this multi-area structure
2052 std::vector<int> sibling_maps;
2053 int parent_id = map->parent();
2054
2055 // Use the same logic as ZScream for area coordination
2056 switch (area_size) {
2057 case AreaSizeEnum::LargeArea: {
2058 // Large Area: 2x2 grid (4 maps total)
2059 // Parent is top-left (quadrant 0), siblings are:
2060 // +1 (top-right, quadrant 1), +8 (bottom-left, quadrant 2), +9 (bottom-right, quadrant 3)
2061 sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
2062 LOG_DEBUG("OverworldEditor",
2063 "RefreshMultiAreaMapsSafely: Large area siblings: %d, %d, %d, %d",
2064 parent_id, parent_id + 1, parent_id + 8, parent_id + 9);
2065 break;
2066 }
2067
2068 case AreaSizeEnum::WideArea: {
2069 // Wide Area: 2x1 grid (2 maps total, horizontally adjacent)
2070 // Parent is left, sibling is +1 (right)
2071 sibling_maps = {parent_id, parent_id + 1};
2072 LOG_DEBUG("OverworldEditor",
2073 "RefreshMultiAreaMapsSafely: Wide area siblings: %d, %d",
2074 parent_id, parent_id + 1);
2075 break;
2076 }
2077
2078 case AreaSizeEnum::TallArea: {
2079 // Tall Area: 1x2 grid (2 maps total, vertically adjacent)
2080 // Parent is top, sibling is +8 (bottom)
2081 sibling_maps = {parent_id, parent_id + 8};
2082 LOG_DEBUG("OverworldEditor",
2083 "RefreshMultiAreaMapsSafely: Tall area siblings: %d, %d",
2084 parent_id, parent_id + 8);
2085 break;
2086 }
2087
2088 default:
2089 LOG_WARN("OverworldEditor",
2090 "RefreshMultiAreaMapsSafely: Unknown area size %d for map %d",
2091 static_cast<int>(area_size), map_index);
2092 return;
2093 }
2094
2095 // Mark all siblings as being processed to prevent recursion
2096 for (int sibling : sibling_maps) {
2097 currently_processing.insert(sibling);
2098 }
2099
2100 // Only refresh siblings that are visible/current and need updating
2101 for (int sibling : sibling_maps) {
2102 if (sibling == map_index) {
2103 continue; // Skip self (already processed above)
2104 }
2105
2106 // Bounds check
2107 if (sibling < 0 || sibling >= zelda3::kNumOverworldMaps) {
2108 continue;
2109 }
2110
2111 // Only refresh if it's visible or current
2112 bool is_current_map = (sibling == current_map_);
2113 bool is_current_world = (sibling / 0x40 == current_world_);
2114 bool needs_refresh = maps_bmp_[sibling].modified();
2115
2116 if ((is_current_map || is_current_world) && needs_refresh) {
2117 LOG_DEBUG("OverworldEditor",
2118 "RefreshMultiAreaMapsSafely: Refreshing %s area sibling map %d "
2119 "(parent: %d)",
2120 (area_size == AreaSizeEnum::LargeArea) ? "large"
2121 : (area_size == AreaSizeEnum::WideArea) ? "wide"
2122 : "tall",
2123 sibling, parent_id);
2124
2125 // Direct refresh without calling RefreshChildMapOnDemand to avoid recursion
2126 auto* sibling_map = overworld_.mutable_overworld_map(sibling);
2127 if (sibling_map && maps_bmp_[sibling].modified()) {
2128 sibling_map->LoadAreaGraphics();
2129
2130 auto status = sibling_map->BuildTileset();
2131 if (status.ok()) {
2132 status = sibling_map->BuildTiles16Gfx(*overworld_.mutable_tiles16(),
2133 overworld_.tiles16().size());
2134 if (status.ok()) {
2135 // Load palette for the sibling map
2136 status = sibling_map->LoadPalette();
2137 if (status.ok()) {
2138 status = sibling_map->BuildBitmap(
2140 if (status.ok()) {
2141 maps_bmp_[sibling].set_data(sibling_map->bitmap_data());
2142
2143 // SAFETY: Only set palette if bitmap has a valid surface
2144 if (maps_bmp_[sibling].is_active() && maps_bmp_[sibling].surface()) {
2145 maps_bmp_[sibling].SetPalette(overworld_.current_area_palette());
2146 }
2147 maps_bmp_[sibling].set_modified(false);
2148
2149 // Queue texture update/creation
2150 if (maps_bmp_[sibling].texture()) {
2153 } else {
2154 EnsureMapTexture(sibling);
2155 }
2156 }
2157 }
2158 }
2159 }
2160
2161 if (!status.ok()) {
2162 LOG_ERROR("OverworldEditor",
2163 "RefreshMultiAreaMapsSafely: Failed to refresh sibling map %d: "
2164 "%s",
2165 sibling, status.message().data());
2166 }
2167 }
2168 } else if (!is_current_map && !is_current_world) {
2169 // Mark non-visible siblings for deferred refresh
2170 maps_bmp_[sibling].set_modified(true);
2171 }
2172 }
2173
2174 // Clear processing set after completion
2175 for (int sibling : sibling_maps) {
2176 currently_processing.erase(sibling);
2177 }
2178}
2179
2183 const auto current_map_palette = overworld_.current_area_palette();
2184
2185 // Check if ZSCustomOverworld v3 is present
2186 uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
2187 bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF);
2188
2189 if (use_v3_area_sizes) {
2190 // Use v3 area size system
2192 auto area_size = overworld_.overworld_map(current_map_)->area_size();
2193
2194 if (area_size != AreaSizeEnum::SmallArea) {
2195 // Get all sibling maps that need palette updates
2196 std::vector<int> sibling_maps;
2197 int parent_id = overworld_.overworld_map(current_map_)->parent();
2198
2199 switch (area_size) {
2200 case AreaSizeEnum::LargeArea:
2201 // 2x2 grid: parent, parent+1, parent+8, parent+9
2202 sibling_maps = {parent_id, parent_id + 1, parent_id + 8,
2203 parent_id + 9};
2204 break;
2205 case AreaSizeEnum::WideArea:
2206 // 2x1 grid: parent, parent+1
2207 sibling_maps = {parent_id, parent_id + 1};
2208 break;
2209 case AreaSizeEnum::TallArea:
2210 // 1x2 grid: parent, parent+8
2211 sibling_maps = {parent_id, parent_id + 8};
2212 break;
2213 default:
2214 break;
2215 }
2216
2217 // Update palette for all siblings
2218 for (int sibling_index : sibling_maps) {
2219 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
2220 continue;
2221 }
2223 overworld_.mutable_overworld_map(sibling_index)->LoadPalette());
2224 maps_bmp_[sibling_index].SetPalette(current_map_palette);
2225 }
2226 } else {
2227 // Small area - only update current map
2228 maps_bmp_[current_map_].SetPalette(current_map_palette);
2229 }
2230 } else {
2231 // Legacy logic for vanilla and v2 ROMs
2232 if (overworld_.overworld_map(current_map_)->is_large_map()) {
2233 // We need to update the map and its siblings if it's a large map
2234 for (int i = 1; i < 4; i++) {
2235 int sibling_index =
2236 overworld_.overworld_map(current_map_)->parent() + i;
2237 if (i >= 2)
2238 sibling_index += 6;
2240 overworld_.mutable_overworld_map(sibling_index)->LoadPalette());
2241
2242 // SAFETY: Only set palette if bitmap has a valid surface
2243 if (maps_bmp_[sibling_index].is_active() && maps_bmp_[sibling_index].surface()) {
2244 maps_bmp_[sibling_index].SetPalette(current_map_palette);
2245 }
2246 }
2247 }
2248
2249 // SAFETY: Only set palette if bitmap has a valid surface
2250 if (maps_bmp_[current_map_].is_active() && maps_bmp_[current_map_].surface()) {
2251 maps_bmp_[current_map_].SetPalette(current_map_palette);
2252 }
2253 }
2254
2255 return absl::OkStatus();
2256}
2257
2259 // Mark the bitmap as modified to force refresh on next update
2260 if (map_index >= 0 && map_index < static_cast<int>(maps_bmp_.size())) {
2261 maps_bmp_[map_index].set_modified(true);
2262
2263 // Clear blockset cache
2264 current_blockset_ = 0xFF;
2265
2266 LOG_DEBUG("OverworldEditor", "ForceRefreshGraphics: Map %d marked for refresh", map_index);
2267 }
2268}
2269
2270void OverworldEditor::RefreshSiblingMapGraphics(int map_index, bool include_self) {
2271 if (map_index < 0 || map_index >= static_cast<int>(maps_bmp_.size())) {
2272 return;
2273 }
2274
2275 auto* map = overworld_.mutable_overworld_map(map_index);
2276 if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
2277 return; // No siblings for small areas
2278 }
2279
2280 int parent_id = map->parent();
2281 std::vector<int> siblings;
2282
2283 switch (map->area_size()) {
2285 siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
2286 break;
2288 siblings = {parent_id, parent_id + 1};
2289 break;
2291 siblings = {parent_id, parent_id + 8};
2292 break;
2293 default:
2294 return;
2295 }
2296
2297 for (int sibling : siblings) {
2298 if (sibling >= 0 && sibling < 0xA0) {
2299 // Skip self unless include_self is true
2300 if (sibling == map_index && !include_self) {
2301 continue;
2302 }
2303
2304 // Mark as modified FIRST before loading
2305 maps_bmp_[sibling].set_modified(true);
2306
2307 // Load graphics from ROM
2308 overworld_.mutable_overworld_map(sibling)->LoadAreaGraphics();
2309
2310 // CRITICAL FIX: Bypass visibility check - force immediate refresh
2311 // Call RefreshChildMapOnDemand() directly instead of RefreshOverworldMapOnDemand()
2312 RefreshChildMapOnDemand(sibling);
2313
2314 LOG_DEBUG("OverworldEditor", "RefreshSiblingMapGraphics: Refreshed sibling map %d", sibling);
2315 }
2316 }
2317}
2318
2320 const auto& current_ow_map = *overworld_.mutable_overworld_map(current_map_);
2321
2322 // Check if ZSCustomOverworld v3 is present
2323 uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
2324 bool use_v3_area_sizes = (asm_version >= 3);
2325
2326 if (use_v3_area_sizes) {
2327 // Use v3 area size system
2329 auto area_size = current_ow_map.area_size();
2330
2331 if (area_size != AreaSizeEnum::SmallArea) {
2332 // Get all sibling maps that need property updates
2333 std::vector<int> sibling_maps;
2334 int parent_id = current_ow_map.parent();
2335
2336 switch (area_size) {
2337 case AreaSizeEnum::LargeArea:
2338 // 2x2 grid: parent+1, parent+8, parent+9 (skip parent itself)
2339 sibling_maps = {parent_id + 1, parent_id + 8, parent_id + 9};
2340 break;
2341 case AreaSizeEnum::WideArea:
2342 // 2x1 grid: parent+1 (skip parent itself)
2343 sibling_maps = {parent_id + 1};
2344 break;
2345 case AreaSizeEnum::TallArea:
2346 // 1x2 grid: parent+8 (skip parent itself)
2347 sibling_maps = {parent_id + 8};
2348 break;
2349 default:
2350 break;
2351 }
2352
2353 // Copy properties from parent map to all siblings
2354 for (int sibling_index : sibling_maps) {
2355 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
2356 continue;
2357 }
2358 auto& map = *overworld_.mutable_overworld_map(sibling_index);
2359 map.set_area_graphics(current_ow_map.area_graphics());
2360 map.set_area_palette(current_ow_map.area_palette());
2361 map.set_sprite_graphics(game_state_,
2362 current_ow_map.sprite_graphics(game_state_));
2363 map.set_sprite_palette(game_state_,
2364 current_ow_map.sprite_palette(game_state_));
2365 map.set_message_id(current_ow_map.message_id());
2366
2367 // CRITICAL FIX: Reload graphics after changing properties
2368 map.LoadAreaGraphics();
2369 }
2370 }
2371 } else {
2372 // Legacy logic for vanilla and v2 ROMs
2373 if (current_ow_map.is_large_map()) {
2374 // We need to copy the properties from the parent map to the children
2375 for (int i = 1; i < 4; i++) {
2376 int sibling_index = current_ow_map.parent() + i;
2377 if (i >= 2) {
2378 sibling_index += 6;
2379 }
2380 auto& map = *overworld_.mutable_overworld_map(sibling_index);
2381 map.set_area_graphics(current_ow_map.area_graphics());
2382 map.set_area_palette(current_ow_map.area_palette());
2383 map.set_sprite_graphics(game_state_,
2384 current_ow_map.sprite_graphics(game_state_));
2385 map.set_sprite_palette(game_state_,
2386 current_ow_map.sprite_palette(game_state_));
2387 map.set_message_id(current_ow_map.message_id());
2388
2389 // CRITICAL FIX: Reload graphics after changing properties
2390 map.LoadAreaGraphics();
2391 }
2392 }
2393 }
2394}
2395
2397 LOG_DEBUG("OverworldEditor", "RefreshTile16Blockset called");
2398 if (current_blockset_ ==
2399 overworld_.overworld_map(current_map_)->area_graphics()) {
2400 return absl::OkStatus();
2401 }
2403
2406
2407 const auto tile16_data = overworld_.tile16_blockset_data();
2408
2411
2412 // Queue texture update for the atlas
2417 // Create texture if it doesn't exist yet
2420 }
2421
2422 return absl::OkStatus();
2423}
2424
2426 // Handle middle-click for map interaction instead of right-click
2427 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle) &&
2428 ImGui::IsItemHovered()) {
2429 // Get the current map from mouse position
2430 auto mouse_position = ow_map_canvas_.drawn_tile_position();
2431 int map_x = mouse_position.x / kOverworldMapSize;
2432 int map_y = mouse_position.y / kOverworldMapSize;
2433 int hovered_map = map_x + map_y * 8;
2434 if (current_world_ == 1) {
2435 hovered_map += 0x40;
2436 } else if (current_world_ == 2) {
2437 hovered_map += 0x80;
2438 }
2439
2440 // Only interact if we're hovering over a valid map
2441 if (hovered_map >= 0 && hovered_map < 0xA0) {
2442 // Toggle map lock or open properties panel
2443 if (current_map_lock_ && current_map_ == hovered_map) {
2444 current_map_lock_ = false;
2445 } else {
2446 current_map_lock_ = true;
2447 current_map_ = hovered_map;
2449 }
2450 }
2451 }
2452
2453 // Handle double-click to open properties panel
2454 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
2455 ImGui::IsItemHovered()) {
2457 }
2458}
2459
2460// Note: SetupOverworldCanvasContextMenu has been removed (Phase 3B).
2461// Context menu is now setup dynamically in DrawOverworldCanvas() via
2462// MapPropertiesSystem::SetupCanvasContextMenu() for context-aware menu items.
2463
2465 if (blockset_selector_) {
2466 blockset_selector_->ScrollToTile(current_tile16_);
2467 return;
2468 }
2469
2470 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
2471 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT window,
2472 // and when called from CheckForSelectRectangle() during overworld canvas rendering,
2473 // it incorrectly scrolls the overworld canvas instead of the tile16 selector.
2474 //
2475 // The blockset_selector_ should always be available in modern code paths.
2476 // If it's not available, we skip scrolling rather than scroll the wrong window.
2477 //
2478 // This fixes the bug where right-clicking to select tiles on the Dark World
2479 // causes the overworld canvas to scroll unexpectedly.
2480}
2481
2483 static bool init_properties = false;
2484
2485 if (!init_properties) {
2486 for (int i = 0; i < 0x40; i++) {
2487 std::string area_graphics_str = absl::StrFormat(
2488 "%02hX", overworld_.overworld_map(i)->area_graphics());
2490 ->push_back(area_graphics_str);
2491
2492 area_graphics_str = absl::StrFormat(
2493 "%02hX", overworld_.overworld_map(i + 0x40)->area_graphics());
2495 ->push_back(area_graphics_str);
2496
2497 std::string area_palette_str =
2498 absl::StrFormat("%02hX", overworld_.overworld_map(i)->area_palette());
2500 ->push_back(area_palette_str);
2501
2502 area_palette_str = absl::StrFormat(
2503 "%02hX", overworld_.overworld_map(i + 0x40)->area_palette());
2505 ->push_back(area_palette_str);
2506 std::string sprite_gfx_str = absl::StrFormat(
2507 "%02hX", overworld_.overworld_map(i)->sprite_graphics(1));
2509 ->push_back(sprite_gfx_str);
2510
2511 sprite_gfx_str = absl::StrFormat(
2512 "%02hX", overworld_.overworld_map(i)->sprite_graphics(2));
2514 ->push_back(sprite_gfx_str);
2515
2516 sprite_gfx_str = absl::StrFormat(
2517 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_graphics(1));
2519 ->push_back(sprite_gfx_str);
2520
2521 sprite_gfx_str = absl::StrFormat(
2522 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_graphics(2));
2524 ->push_back(sprite_gfx_str);
2525
2526 std::string sprite_palette_str = absl::StrFormat(
2527 "%02hX", overworld_.overworld_map(i)->sprite_palette(1));
2529 ->push_back(sprite_palette_str);
2530
2531 sprite_palette_str = absl::StrFormat(
2532 "%02hX", overworld_.overworld_map(i)->sprite_palette(2));
2534 ->push_back(sprite_palette_str);
2535
2536 sprite_palette_str = absl::StrFormat(
2537 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_palette(1));
2539 ->push_back(sprite_palette_str);
2540
2541 sprite_palette_str = absl::StrFormat(
2542 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_palette(2));
2544 ->push_back(sprite_palette_str);
2545 }
2546 init_properties = true;
2547 }
2548
2549 Text("Area Gfx LW/DW");
2550 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2552 SameLine();
2553 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2555 ImGui::Separator();
2556
2557 Text("Sprite Gfx LW/DW");
2558 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2560 SameLine();
2561 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2563 SameLine();
2564 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2566 SameLine();
2567 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2569 ImGui::Separator();
2570
2571 Text("Area Pal LW/DW");
2572 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2574 SameLine();
2575 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2577
2578 static bool show_gfx_group = false;
2579 Checkbox("Show Gfx Group Editor", &show_gfx_group);
2580 if (show_gfx_group) {
2581 gui::BeginWindowWithDisplaySettings("Gfx Group Editor", &show_gfx_group);
2584 }
2585}
2586
2588 if (BeginTable("UsageStatsTable", 3,
2589 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter,
2590 ImVec2(0, 0))) {
2591 TableSetupColumn("Entrances");
2592 TableSetupColumn("Grid", ImGuiTableColumnFlags_WidthStretch,
2593 ImGui::GetContentRegionAvail().x);
2594 TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 256);
2595 TableHeadersRow();
2596 TableNextRow();
2597
2598 TableNextColumn();
2599 if (BeginChild("UnusedSpritesetScroll", ImVec2(0, 0), true,
2600 ImGuiWindowFlags_HorizontalScrollbar)) {
2601 for (int i = 0; i < 0x81; i++) {
2602 auto entrance_name = rom_->resource_label()->CreateOrGetLabel(
2603 "Dungeon Entrance Names", util::HexByte(i),
2605 std::string str = absl::StrFormat("%#x - %s", i, entrance_name);
2606 if (Selectable(str.c_str(), selected_entrance_ == i,
2607 overworld_.entrances().at(i).deleted
2608 ? ImGuiSelectableFlags_Disabled
2609 : 0)) {
2611 selected_usage_map_ = overworld_.entrances().at(i).map_id_;
2613 }
2614 if (IsItemHovered()) {
2615 BeginTooltip();
2616 Text("Entrance ID: %d", i);
2617 Text("Map ID: %d", overworld_.entrances().at(i).map_id_);
2618 Text("Entrance ID: %d", overworld_.entrances().at(i).entrance_id_);
2619 Text("X: %d", overworld_.entrances().at(i).x_);
2620 Text("Y: %d", overworld_.entrances().at(i).y_);
2621 Text("Deleted? %s",
2622 overworld_.entrances().at(i).deleted ? "Yes" : "No");
2623 EndTooltip();
2624 }
2625 }
2626 EndChild();
2627 }
2628
2629 TableNextColumn();
2630 DrawUsageGrid();
2631
2632 TableNextColumn();
2634
2635 EndTable();
2636 }
2637 return absl::OkStatus();
2638}
2639
2641 // Create a grid of 8x8 squares
2642 int total_squares = 128;
2643 int squares_wide = 8;
2644 int squares_tall = (total_squares + squares_wide - 1) /
2645 squares_wide; // Ceiling of total_squares/squares_wide
2646
2647 // Loop through each row
2648 for (int row = 0; row < squares_tall; ++row) {
2649 NewLine();
2650
2651 for (int col = 0; col < squares_wide; ++col) {
2652 if (row * squares_wide + col >= total_squares) {
2653 break;
2654 }
2655 // Determine if this square should be highlighted
2656 bool highlight = selected_usage_map_ == (row * squares_wide + col);
2657
2658 // Set highlight color if needed
2659 if (highlight) {
2660 PushStyleColor(ImGuiCol_Button, gui::GetSelectedColor());
2661 }
2662
2663 // Create a button or selectable for each square
2664 if (Button("##square", ImVec2(20, 20))) {
2665 // Switch over to the room editor tab
2666 // and add a room tab by the ID of the square
2667 // that was clicked
2668 }
2669
2670 // Reset style if it was highlighted
2671 if (highlight) {
2672 PopStyleColor();
2673 }
2674
2675 // Check if the square is hovered
2676 if (IsItemHovered()) {
2677 // Display a tooltip with all the room properties
2678 }
2679
2680 // Keep squares in the same line
2681 SameLine();
2682 }
2683 }
2684}
2685
2687 Text("Current Map: %d", current_map_);
2688 Text("Current Tile16: %d", current_tile16_);
2689 int relative_x = (int)ow_map_canvas_.drawn_tile_position().x % 512;
2690 int relative_y = (int)ow_map_canvas_.drawn_tile_position().y % 512;
2691 Text("Current Tile16 Drawn Position (Relative): %d, %d", relative_x,
2692 relative_y);
2693
2694 // Print the size of the overworld map_tiles per world
2695 Text("Light World Map Tiles: %d",
2696 (int)overworld_.mutable_map_tiles()->light_world.size());
2697 Text("Dark World Map Tiles: %d",
2698 (int)overworld_.mutable_map_tiles()->dark_world.size());
2699 Text("Special World Map Tiles: %d",
2700 (int)overworld_.mutable_map_tiles()->special_world.size());
2701
2702 static bool view_lw_map_tiles = false;
2703 static MemoryEditor mem_edit;
2704 // Let's create buttons which let me view containers in the memory editor
2705 if (Button("View Light World Map Tiles")) {
2706 view_lw_map_tiles = !view_lw_map_tiles;
2707 }
2708
2709 if (view_lw_map_tiles) {
2710 mem_edit.DrawContents(
2711 overworld_.mutable_map_tiles()->light_world[current_map_].data(),
2712 overworld_.mutable_map_tiles()->light_world[current_map_].size());
2713 }
2714}
2715
2718 current_graphics_set_.clear();
2719 all_gfx_loaded_ = false;
2720 map_blockset_loaded_ = false;
2721 return absl::OkStatus();
2722}
2723
2724absl::Status OverworldEditor::ApplyZSCustomOverworldASM(int target_version) {
2725 // Feature flag deprecated - ROM version gating is sufficient
2726 // User explicitly clicked upgrade button, so respect their request
2727
2728 // Validate target version
2729 if (target_version < 2 || target_version > 3) {
2730 return absl::InvalidArgumentError(absl::StrFormat(
2731 "Invalid target version: %d. Must be 2 or 3.", target_version));
2732 }
2733
2734 // Check current ROM version
2735 uint8_t current_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
2736 if (current_version != 0xFF && current_version >= target_version) {
2737 return absl::AlreadyExistsError(absl::StrFormat(
2738 "ROM is already version %d or higher", current_version));
2739 }
2740
2741 LOG_DEBUG("OverworldEditor", "Applying ZSCustomOverworld ASM v%d to ROM...",
2742 target_version);
2743
2744 // Initialize Asar wrapper
2745 auto asar_wrapper = std::make_unique<core::AsarWrapper>();
2746 RETURN_IF_ERROR(asar_wrapper->Initialize());
2747
2748 // Create backup of ROM data
2749 std::vector<uint8_t> original_rom_data = rom_->vector();
2750 std::vector<uint8_t> working_rom_data = original_rom_data;
2751
2752 try {
2753 // Determine which ASM file to apply and use GetResourcePath for proper resolution
2754 std::string asm_file_name = (target_version == 3)
2755 ? "asm/yaze.asm" // Master file with v3
2756 : "asm/ZSCustomOverworld.asm"; // v2 standalone
2757
2758 // Use GetResourcePath to handle app bundles and various deployment scenarios
2759 std::string asm_file_path = util::GetResourcePath(asm_file_name);
2760
2761 LOG_DEBUG("OverworldEditor", "Using ASM file: %s", asm_file_path.c_str());
2762
2763 // Verify file exists
2764 if (!std::filesystem::exists(asm_file_path)) {
2765 return absl::NotFoundError(absl::StrFormat(
2766 "ASM file not found at: %s\n\n"
2767 "Expected location: assets/%s\n"
2768 "Make sure the assets directory is accessible.",
2769 asm_file_path, asm_file_name));
2770 }
2771
2772 // Apply the ASM patch
2773 auto patch_result =
2774 asar_wrapper->ApplyPatch(asm_file_path, working_rom_data);
2775 if (!patch_result.ok()) {
2776 return absl::InternalError(absl::StrFormat(
2777 "Failed to apply ASM patch: %s", patch_result.status().message()));
2778 }
2779
2780 const auto& result = patch_result.value();
2781 if (!result.success) {
2782 std::string error_details = "ASM patch failed with errors:\n";
2783 for (const auto& error : result.errors) {
2784 error_details += " - " + error + "\n";
2785 }
2786 if (!result.warnings.empty()) {
2787 error_details += "Warnings:\n";
2788 for (const auto& warning : result.warnings) {
2789 error_details += " - " + warning + "\n";
2790 }
2791 }
2792 return absl::InternalError(error_details);
2793 }
2794
2795 // Update ROM with patched data
2796 RETURN_IF_ERROR(rom_->LoadFromData(working_rom_data, false));
2797
2798 // Update version marker and feature flags
2800
2801 // Log symbols found during patching
2802 LOG_DEBUG("OverworldEditor", "ASM patch applied successfully. Found %zu symbols:",
2803 result.symbols.size());
2804 for (const auto& symbol : result.symbols) {
2805 LOG_DEBUG("OverworldEditor", " %s @ $%06X", symbol.name.c_str(),
2806 symbol.address);
2807 }
2808
2809 // Refresh overworld data to reflect changes
2811
2812 LOG_DEBUG("OverworldEditor", "ZSCustomOverworld v%d successfully applied to ROM",
2813 target_version);
2814 return absl::OkStatus();
2815
2816 } catch (const std::exception& e) {
2817 // Restore original ROM data on any exception
2818 auto restore_result = rom_->LoadFromData(original_rom_data, false);
2819 if (!restore_result.ok()) {
2820 LOG_ERROR("OverworldEditor", "Failed to restore ROM data: %s",
2821 restore_result.message().data());
2822 }
2823 return absl::InternalError(
2824 absl::StrFormat("Exception during ASM application: %s", e.what()));
2825 }
2826}
2827
2828absl::Status OverworldEditor::UpdateROMVersionMarkers(int target_version) {
2829 // Set the main version marker
2831 static_cast<uint8_t>(target_version);
2832
2833 // Enable feature flags based on target version
2834 if (target_version >= 2) {
2835 // v2+ features
2838
2839 LOG_DEBUG("OverworldEditor", "Enabled v2+ features: Custom BG colors, Main palettes");
2840 }
2841
2842 if (target_version >= 3) {
2843 // v3 features
2848
2849 LOG_DEBUG("OverworldEditor",
2850 "Enabled v3+ features: Subscreen overlays, Animated GFX, Tile GFX "
2851 "groups, Mosaic");
2852
2853 // Initialize area size data for v3 (set all areas to small by default)
2854 for (int i = 0; i < 0xA0; i++) {
2855 (*rom_)[zelda3::kOverworldScreenSize + i] =
2856 static_cast<uint8_t>(zelda3::AreaSizeEnum::SmallArea);
2857 }
2858
2859 // Set appropriate sizes for known large areas
2860 const std::vector<int> large_areas = {
2861 0x00, 0x02, 0x05, 0x07, 0x0A, 0x0B, 0x0F, 0x10, 0x11, 0x12,
2862 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1D,
2863 0x1E, 0x25, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x30,
2864 0x32, 0x33, 0x34, 0x35, 0x37, 0x3A, 0x3B, 0x3C, 0x3F};
2865
2866 for (int area_id : large_areas) {
2867 if (area_id < 0xA0) {
2868 (*rom_)[zelda3::kOverworldScreenSize + area_id] =
2869 static_cast<uint8_t>(zelda3::AreaSizeEnum::LargeArea);
2870 }
2871 }
2872
2873 LOG_DEBUG("OverworldEditor", "Initialized area size data for %zu areas",
2874 large_areas.size());
2875 }
2876
2877 LOG_DEBUG("OverworldEditor", "ROM version markers updated to v%d", target_version);
2878 return absl::OkStatus();
2879}
2880
2882 if (!blockset_selector_) {
2883 return;
2884 }
2885
2887 blockset_selector_->SetSelectedTile(current_tile16_);
2888}
2889
2890// ============================================================================
2891// Canvas Automation API Integration (Phase 4)
2892// ============================================================================
2893
2895 auto* api = ow_map_canvas_.GetAutomationAPI();
2896
2897 // Set tile paint callback
2898 api->SetTilePaintCallback([this](int x, int y, int tile_id) {
2899 return AutomationSetTile(x, y, tile_id);
2900 });
2901
2902 // Set tile query callback
2903 api->SetTileQueryCallback([this](int x, int y) {
2904 return AutomationGetTile(x, y);
2905 });
2906}
2907
2908bool OverworldEditor::AutomationSetTile(int x, int y, int tile_id) {
2909 if (!overworld_.is_loaded()) {
2910 return false;
2911 }
2912
2913 // Bounds check
2914 if (x < 0 || y < 0 || x >= 512 || y >= 512) {
2915 return false;
2916 }
2917
2918 // Set current world based on current_map_
2921
2922 // Set the tile in the overworld data structure
2923 overworld_.SetTile(x, y, static_cast<uint16_t>(tile_id));
2924
2925 // Update the bitmap
2926 auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile_id);
2927 if (!tile_data.empty()) {
2928 RenderUpdatedMapBitmap(ImVec2(static_cast<float>(x * 16),
2929 static_cast<float>(y * 16)), tile_data);
2930 return true;
2931 }
2932
2933 return false;
2934}
2935
2937 if (!overworld_.is_loaded()) {
2938 return -1;
2939 }
2940
2941 // Bounds check
2942 if (x < 0 || y < 0 || x >= 512 || y >= 512) {
2943 return -1;
2944 }
2945
2946 // Set current world
2949
2950 return overworld_.GetTile(x, y);
2951}
2952
2953} // namespace yaze::editor
absl::Status LoadFromData(const std::vector< uint8_t > &data, bool z3_load=true)
Definition rom.cc:381
auto vector() const
Definition rom.h:207
void set_dirty(bool dirty)
Definition rom.h:199
core::ResourceLabelManager * resource_label()
Definition rom.h:220
bool is_loaded() const
Definition rom.h:197
static Flags & get()
Definition features.h:79
std::string MakeCardTitle(const std::string &base_title) const
Definition editor.h:127
EditorContext * context_
Definition editor.h:124
absl::Status Clear() override
std::unique_ptr< MapPropertiesSystem > map_properties_system_
zelda3::OverworldItem current_item_
zelda3::OverworldEntranceTileTypes entrance_tiletypes_
zelda3::OverworldEntrance current_entrance_
absl::Status ApplyZSCustomOverworldASM(int target_version)
Apply ZSCustomOverworld ASM patch to upgrade ROM version.
absl::Status CheckForCurrentMap()
Check for changes to the overworld map. Calls RefreshOverworldMap and RefreshTile16Blockset on the cu...
void ForceRefreshGraphics(int map_index)
std::vector< int > selected_tile16_ids_
zelda3::GameEntity * dragged_entity_
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > maps_bmp_
void CheckForOverworldEdits()
Check for changes to the overworld map.
absl::Status UpdateROMVersionMarkers(int target_version)
Update ROM version markers and feature flags after ASM patching.
zelda3::OverworldExit current_exit_
void RefreshSiblingMapGraphics(int map_index, bool include_self=false)
void RenderUpdatedMapBitmap(const ImVec2 &click_position, const std::vector< uint8_t > &tile_data)
void RefreshOverworldMapOnDemand(int map_index)
On-demand map refresh that only updates what's actually needed.
bool AutomationSetTile(int x, int y, int tile_id)
void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap *map)
Safely refresh multi-area maps without recursion.
zelda3::Overworld & overworld()
void CheckForSelectRectangle()
Draw and create the tile16 IDs that are currently selected.
std::unique_ptr< OverworldEntityRenderer > entity_renderer_
absl::Status Load() override
void EnsureMapTexture(int map_index)
Ensure a specific map has its texture created.
std::vector< gfx::Bitmap > sprite_previews_
void ProcessDeferredTextures()
Create textures for deferred map bitmaps on demand.
std::unique_ptr< gui::TileSelectorWidget > blockset_selector_
absl::Status Paste() override
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset canvas to show the current selected tile16.
absl::Status LoadGraphics()
Load the Bitmap objects for each OverworldMap.
void RefreshChildMapOnDemand(int map_index)
On-demand child map refresh with selective updates.
zelda3::GameEntity * current_entity_
absl::Status Initialize(const gfx::Bitmap &tile16_blockset_bmp, const gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
absl::Status SetCurrentTile(int id)
void set_palette(const gfx::SnesPalette &palette)
void set_on_changes_committed(std::function< absl::Status()> callback)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:32
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:36
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:78
static Arena & Get()
Definition arena.cc:15
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:66
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:377
TextureHandle texture() const
Definition bitmap.h:260
const std::vector< uint8_t > & vector() const
Definition bitmap.h:261
auto size() const
Definition bitmap.h:256
bool is_active() const
Definition bitmap.h:264
void set_modified(bool modified)
Definition bitmap.h:267
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap.
Definition bitmap.cc:292
SDL_Surface * surface() const
Definition bitmap.h:259
RAII timer for automatic timing management.
void SetTilePaintCallback(TilePaintCallback callback)
void set_scrolling(ImVec2 scroll)
Definition canvas.h:312
auto selected_tile_pos() const
Definition canvas.h:343
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1062
auto global_scale() const
Definition canvas.h:345
auto select_rect_active() const
Definition canvas.h:341
auto selected_tiles() const
Definition canvas.h:342
void DrawBitmapGroup(std::vector< int > &group, gfx::Tilemap &tilemap, int tile_size, float scale=1.0f, int local_map_size=0x200, ImVec2 total_map_size=ImVec2(0x1000, 0x1000))
Draw group of bitmaps for multi-tile selection preview.
Definition canvas.cc:1157
CanvasAutomationAPI * GetAutomationAPI()
Definition canvas.cc:1922
auto hover_mouse_pos() const
Definition canvas.h:402
void UpdateInfoGrid(ImVec2 bg_size, float grid_size=64.0f, int label_id=0)
Definition canvas.cc:373
void DrawContextMenu()
Definition canvas.cc:441
auto drawn_tile_position() const
Definition canvas.h:313
bool DrawTilemapPainter(gfx::Tilemap &tilemap, int current_tile)
Definition canvas.cc:774
auto draw_list() const
Definition canvas.h:309
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:920
auto mutable_labels(int i)
Definition canvas.h:383
auto canvas_size() const
Definition canvas.h:314
void set_selected_tile_pos(ImVec2 pos)
Definition canvas.h:344
void set_global_scale(float scale)
Definition canvas.h:315
void DrawSelectRect(int current_map, int tile_size=0x10, float scale=1.0f)
Definition canvas.cc:948
auto zero_point() const
Definition canvas.h:310
bool IsMouseHovering() const
Definition canvas.h:301
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1142
auto scrolling() const
Definition canvas.h:311
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:381
const ImVector< ImVec2 > & points() const
Definition canvas.h:306
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1386
auto selected_points() const
Definition canvas.h:400
auto set_highlight_tile_id(int i)
Definition canvas.h:396
static EditorCardManager & Get()
Draggable, dockable card for editor sub-windows.
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
void SetPosition(Position pos)
Ultra-compact toolbar that merges mode buttons with settings.
bool ModeButton(const char *icon, bool selected, const char *tooltip)
bool AddUsageStatsButton(const char *tooltip)
bool AddProperty(const char *icon, const char *label, uint8_t *value, std::function< void()> on_change=nullptr)
bool AddAction(const char *icon, const char *tooltip)
void AddRomBadge(uint8_t version, std::function< void()> on_upgrade=nullptr)
bool AddToggle(const char *icon, bool *state, const char *tooltip)
enum yaze::zelda3::GameEntity::EntityType entity_type_
virtual void UpdateMapProperties(uint16_t map_id)=0
Represents a single Overworld map screen.
auto tile16_blockset_data() const
Definition overworld.h:284
auto current_area_palette() const
Definition overworld.h:278
void set_current_world(int world)
Definition overworld.h:292
int GetTileFromPosition(ImVec2 position) const
Definition overworld.h:234
absl::Status Load(Rom *rom)
Definition overworld.cc:27
absl::Status SaveMapProperties()
absl::Status SaveMap32Tiles()
absl::Status SaveMap16Tiles()
std::vector< gfx::Tile16 > tiles16() const
Definition overworld.h:262
auto is_loaded() const
Definition overworld.h:287
absl::Status CreateTile32Tilemap()
auto overworld_map(int i) const
Definition overworld.h:258
void set_current_map(int i)
Definition overworld.h:291
auto mutable_overworld_map(int i)
Definition overworld.h:259
absl::Status SaveEntrances()
absl::Status SaveExits()
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
Definition overworld.cc:656
absl::Status SaveItems()
uint16_t GetTile(int x, int y) const
Definition overworld.h:293
absl::Status SaveOverworldMaps()
Definition overworld.cc:960
void SetTile(int x, int y, uint16_t tile_id)
Definition overworld.h:302
auto mutable_sprites(int state)
Definition overworld.h:266
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:270
auto current_map_bitmap_data() const
Definition overworld.h:281
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:244
absl::Status SaveMusic()
A class for managing sprites in the overworld and underworld.
Definition sprite.h:279
#define ICON_MD_GRID_VIEW
Definition icons.h:895
#define ICON_MD_SETTINGS
Definition icons.h:1697
#define ICON_MD_CANCEL
Definition icons.h:362
#define ICON_MD_UPGRADE
Definition icons.h:2045
#define ICON_MD_CHECK
Definition icons.h:395
#define ICON_MD_COLLECTIONS
Definition icons.h:436
#define ICON_MD_DRAW
Definition icons.h:623
#define ICON_MD_BRUSH
Definition icons.h:323
#define ICON_MD_TUNE
Definition icons.h:2020
#define ICON_MD_ZOOM_OUT
Definition icons.h:2194
#define ICON_MD_OPEN_IN_FULL
Definition icons.h:1351
#define ICON_MD_GRID_3X3
Definition icons.h:890
#define ICON_MD_GRASS
Definition icons.h:889
#define ICON_MD_FORMAT_COLOR_FILL
Definition icons.h:828
#define ICON_MD_DOOR_BACK
Definition icons.h:610
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1262
#define ICON_MD_GRID_ON
Definition icons.h:894
#define ICON_MD_LAYERS
Definition icons.h:1066
#define ICON_MD_DOOR_FRONT
Definition icons.h:611
#define ICON_MD_IMAGE
Definition icons.h:980
#define ICON_MD_ADD_LOCATION
Definition icons.h:98
#define ICON_MD_ZOOM_IN
Definition icons.h:2192
#define ICON_MD_GRID_4X4
Definition icons.h:891
#define ICON_MD_MOUSE
Definition icons.h:1249
#define ICON_MD_FOLDER
Definition icons.h:807
#define ICON_MD_PALETTE
Definition icons.h:1368
#define ICON_MD_PEST_CONTROL_RODENT
Definition icons.h:1428
#define ICON_MD_ANALYTICS
Definition icons.h:152
#define LOG_DEBUG(category, format,...)
Definition log.h:104
#define LOG_ERROR(category, format,...)
Definition log.h:110
#define LOG_WARN(category, format,...)
Definition log.h:108
#define PRINT_IF_ERROR(expression)
Definition macro.h:27
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
Definition input.cc:20
ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size)
Editors are the view controllers for the application.
void MoveEntityOnGrid(zelda3::GameEntity *entity, ImVec2 canvas_p0, ImVec2 scrolling, bool free_movement)
Definition entity.cc:36
constexpr unsigned int kOverworldMapSize
bool DrawSpriteEditorPopup(zelda3::Sprite &sprite)
Definition entity.cc:436
bool DrawOverworldEntrancePopup(zelda3::OverworldEntrance &entrance)
Definition entity.cc:82
bool DrawItemEditorPopup(zelda3::OverworldItem &item)
Definition entity.cc:315
constexpr int kTile16Size
constexpr float kInputFieldSize
Definition entity.cc:19
bool DrawExitEditorPopup(zelda3::OverworldExit &exit)
Definition entity.cc:153
void UpdateTilemap(IRenderer *renderer, Tilemap &tilemap, const std::vector< uint8_t > &data)
Definition tilemap.cc:32
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:235
Tilemap CreateTilemap(IRenderer *renderer, std::vector< uint8_t > &data, int width, int height, int tile_size, int num_tiles, SnesPalette &palette)
Definition tilemap.cc:14
void VerticalSpacing(float pixels)
void BeginPadding(int i)
Definition style.cc:272
void BeginChildBothScrollbars(int id)
Definition style.cc:309
ImVec4 GetSelectedColor()
Definition ui_helpers.cc:66
void EndNoPadding()
Definition style.cc:282
void CenterText(const char *text)
void EndPadding()
Definition style.cc:276
void BeginNoPadding()
Definition style.cc:278
void EndWindowWithDisplaySettings()
Definition style.cc:267
void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size, ImGuiWindowFlags flags)
Definition style.cc:247
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:284
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
std::string GetResourcePath(const std::string &resource_path)
Definition file_util.cc:70
constexpr int OverworldCustomTileGFXGroupEnabled
constexpr int OverworldCustomAreaSpecificBGEnabled
constexpr int kOverworldScreenSize
Definition overworld.h:66
constexpr int kNumTile16Individual
Definition overworld.h:120
constexpr int kSpecialWorldMapIdStart
constexpr int OverworldCustomAnimatedGFXEnabled
constexpr int OverworldCustomMainPaletteEnabled
constexpr int kNumOverworldMaps
Definition overworld.h:119
constexpr int OverworldCustomASMHasBeenApplied
constexpr int kDarkWorldMapIdStart
absl::StatusOr< OverworldEntranceTileTypes > LoadEntranceTileTypes(Rom *rom)
constexpr int OverworldCustomMosaicEnabled
constexpr const char * kEntranceNames[]
Definition common.h:47
constexpr int OverworldCustomSubscreenOverlayEnabled
#define IM_PI
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:874
struct yaze::editor::EditorContext::SharedClipboard shared_clipboard
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:110