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// Related header
3
4#ifndef IM_PI
5#define IM_PI 3.14159265358979323846f
6#endif
7
8// C system headers
9#include <cmath>
10#include <cstddef>
11#include <cstdint>
12
13// C++ standard library headers
14#include <algorithm>
15#include <exception>
16#include <filesystem>
17#include <iostream>
18#include <memory>
19#include <new>
20#include <ostream>
21#include <string>
22#include <unordered_map>
23#include <utility>
24#include <vector>
25
26// Third-party library headers
27#include "absl/status/status.h"
28#include "absl/strings/str_format.h"
29#include "imgui/imgui.h"
30
31// Project headers
54#include "app/gfx/core/bitmap.h"
63#include "app/gui/core/icons.h"
65#include "app/gui/core/style.h"
69#include "core/asar_wrapper.h"
70#include "core/features.h"
71#include "rom/rom.h"
72#include "util/file_util.h"
73#include "util/hex.h"
74#include "util/log.h"
75#include "util/macro.h"
76#include "zelda3/common.h"
84
85namespace yaze::editor {
86
88 // Register panels with PanelManager (dependency injection)
90 return;
91 }
92 auto* panel_manager = dependencies_.panel_manager;
93
94 // Initialize renderer from dependencies
96
97 // Register Overworld Canvas (main canvas panel with toolset)
98
99 // Register EditorPanel instances (new architecture)
100 panel_manager->RegisterEditorPanel(std::make_unique<AreaGraphicsPanel>(this));
101 panel_manager->RegisterEditorPanel(
102 std::make_unique<Tile16SelectorPanel>(this));
103 panel_manager->RegisterEditorPanel(
104 std::make_unique<Tile16EditorPanel>(&tile16_editor_));
105 panel_manager->RegisterEditorPanel(
106 std::make_unique<MapPropertiesPanel>(this));
107 panel_manager->RegisterEditorPanel(std::make_unique<ScratchSpacePanel>(this));
108 panel_manager->RegisterEditorPanel(
109 std::make_unique<UsageStatisticsPanel>(this));
110 panel_manager->RegisterEditorPanel(
111 std::make_unique<Tile8SelectorPanel>(this));
112 panel_manager->RegisterEditorPanel(std::make_unique<DebugWindowPanel>(this));
113 panel_manager->RegisterEditorPanel(std::make_unique<GfxGroupsPanel>(this));
114 panel_manager->RegisterEditorPanel(std::make_unique<V3SettingsPanel>(this));
115
116 panel_manager->RegisterEditorPanel(
117 std::make_unique<OverworldCanvasPanel>(this));
118
119 // Note: Legacy RegisterPanel() calls removed.
120 // RegisterEditorPanel() auto-creates PanelDescriptor entries for each panel,
121 // eliminating the dual registration problem identified in the panel system audit.
122 // Panel visibility is now managed centrally through PanelManager.
123
124 // Original initialization code below:
125 // Initialize MapPropertiesSystem with canvas and bitmap data
126 // Initialize cards
127 usage_stats_card_ = std::make_unique<UsageStatisticsCard>(&overworld_);
128 debug_window_card_ = std::make_unique<DebugWindowCard>();
129
130 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
132
133 // Set up refresh callbacks for MapPropertiesSystem
134 map_properties_system_->SetRefreshCallbacks(
135 [this]() { this->RefreshMapProperties(); },
136 [this]() { this->RefreshOverworldMap(); },
137 [this]() -> absl::Status { return this->RefreshMapPalette(); },
138 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
139 [this](int map_index) { this->ForceRefreshGraphics(map_index); });
140
141 // Initialize OverworldSidebar
142 sidebar_ = std::make_unique<OverworldSidebar>(&overworld_, rom_,
144
145 // Initialize OverworldEntityRenderer for entity visualization
146 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
148
149 // Initialize Toolbar
150 toolbar_ = std::make_unique<OverworldToolbar>();
151 toolbar_->on_refresh_graphics = [this]() {
152 // Invalidate cached graphics for the current map area to force re-render
153 // with potentially new palette/graphics settings
156 };
157 toolbar_->on_refresh_map = [this]() {
159 };
160
161 toolbar_->on_save_to_scratch = [this]() {
163 };
164 toolbar_->on_load_from_scratch = [this]() {
166 };
167
169}
170
171absl::Status OverworldEditor::Load() {
172 gfx::ScopedTimer timer("OverworldEditor::Load");
173
174 LOG_DEBUG("OverworldEditor", "Loading overworld.");
175 if (!rom_ || !rom_->is_loaded()) {
176 return absl::FailedPreconditionError("ROM not loaded");
177 }
178
179 // Clear undo/redo state when loading new ROM data
180 undo_stack_.clear();
181 redo_stack_.clear();
183
188
189 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
192
193 // Set up callback for when tile16 changes are committed
194 tile16_editor_.set_on_changes_committed([this]() -> absl::Status {
195 // Regenerate the overworld editor's tile16 blockset
197
198 // Force refresh of the current overworld map to show changes
200
201 LOG_DEBUG("OverworldEditor",
202 "Overworld editor refreshed after Tile16 changes");
203 return absl::OkStatus();
204 });
205
206 // Set up entity insertion callback for MapPropertiesSystem
208 map_properties_system_->SetEntityCallbacks(
209 [this](const std::string& entity_type) {
210 HandleEntityInsertion(entity_type);
211 });
212
213 // Set up tile16 edit callback for context menu in MOUSE mode
214 map_properties_system_->SetTile16EditCallback(
215 [this]() { HandleTile16Edit(); });
216 }
217
219
220 // Register as palette listener to refresh graphics when palettes change
221 if (palette_listener_id_ < 0) {
223 [this](const std::string& group_name, int palette_index) {
224 // Only respond to overworld-related palette changes
225 if (group_name == "ow_main" || group_name == "ow_animated" ||
226 group_name == "ow_aux" || group_name == "grass") {
227 LOG_DEBUG("OverworldEditor",
228 "Palette change detected: %s, refreshing current map",
229 group_name.c_str());
230 // Refresh current map graphics to reflect palette changes
231 if (current_map_ >= 0 && all_gfx_loaded_) {
233 }
234 }
235 });
236 LOG_DEBUG("OverworldEditor", "Registered as palette listener (ID: %d)",
238 }
239
240 all_gfx_loaded_ = true;
241 return absl::OkStatus();
242}
243
245 status_ = absl::OkStatus();
246
247 // Safety check: Ensure ROM is loaded and graphics are ready
248 if (!rom_ || !rom_->is_loaded()) {
249 gui::CenterText("No ROM loaded");
250 return absl::OkStatus();
251 }
252
253 if (!all_gfx_loaded_) {
254 gui::CenterText("Loading graphics...");
255 return absl::OkStatus();
256 }
257
258 // Process deferred textures for smooth loading
260
261 // Update blockset atlas with any pending tile16 changes for live preview
262 // Tile cache now uses copy semantics so this is safe to enable
265 }
266
267 // Early return if panel_manager is not available
268 // (panels won't be drawn without it, so no point continuing)
270 return status_;
271 }
272
274 return status_;
275 }
276
277 // ===========================================================================
278 // Main Overworld Canvas
279 // ===========================================================================
280 // The panels (Tile16 Selector, Area Graphics, etc.) are now managed by
281 // EditorPanel/PanelManager and drawn automatically. This section only
282 // handles the main canvas and toolbar.
283
284 // ===========================================================================
285 // Non-Panel Windows (not managed by EditorPanel system)
286 // ===========================================================================
287 // These are separate feature windows, not part of the panel system
288
289 // Custom Background Color Editor
291 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
292 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color",
294 if (rom_->is_loaded() && overworld_.is_loaded() &&
296 map_properties_system_->DrawCustomBackgroundColorEditor(
298 }
299 }
300 ImGui::End();
301 }
302
303 // Visual Effects Editor (Subscreen Overlays)
305 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
306 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor",
308 if (rom_->is_loaded() && overworld_.is_loaded() &&
310 map_properties_system_->DrawOverlayEditor(current_map_,
312 }
313 }
314 ImGui::End();
315 }
316
317 // Note: Tile16 Editor is now managed as an EditorPanel (Tile16EditorPanel)
318 // It uses UpdateAsPanel() which provides a context menu instead of MenuBar
319
320 // ===========================================================================
321 // Centralized Entity Interaction Logic (extracted to dedicated method)
322 // ===========================================================================
324
325 // Entity insertion error popup
326 if (ImGui::BeginPopupModal("Entity Insert Error", nullptr,
327 ImGuiWindowFlags_AlwaysAutoResize)) {
328 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
329 ICON_MD_ERROR " Entity Insertion Failed");
330 ImGui::Separator();
331 ImGui::TextWrapped("%s", entity_insert_error_message_.c_str());
332 ImGui::Separator();
333 ImGui::TextDisabled("Tip: Delete an existing entity to free up a slot.");
334 ImGui::Spacing();
335 if (ImGui::Button("OK", ImVec2(120, 0))) {
337 ImGui::CloseCurrentPopup();
338 }
339 ImGui::EndPopup();
340 }
341 // --- END CENTRALIZED LOGIC ---
342
343 // ROM Upgrade Popup (rendered outside toolbar to avoid ID conflicts)
344 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr,
345 ImGuiWindowFlags_AlwaysAutoResize)) {
346 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
347 ImGui::Separator();
348 ImGui::TextWrapped(
349 "This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
350 "enabling advanced features like custom tile graphics, animated GFX,\n"
351 "wide/tall areas, and more.");
352 ImGui::Separator();
353
354 uint8_t current_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
355 ImGui::Text("Current Version: %s",
356 current_version == 0xFF
357 ? "Vanilla"
358 : absl::StrFormat("v%d", current_version).c_str());
359
360 static int target_version = 3;
361 ImGui::RadioButton("v2 (Basic features)", &target_version, 2);
362 ImGui::SameLine();
363 ImGui::RadioButton("v3 (All features)", &target_version, 3);
364
365 ImGui::Separator();
366
367 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150, 0))) {
368 auto status = ApplyZSCustomOverworldASM(target_version);
369 if (status.ok()) {
370 // CRITICAL: Reload the editor to reflect changes
371 status_ = Clear();
372 status_ = Load();
373 ImGui::CloseCurrentPopup();
374 } else {
375 LOG_ERROR("OverworldEditor", "Upgrade failed: %s",
376 status.message().data());
377 }
378 }
379 ImGui::SameLine();
380 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150, 0))) {
381 ImGui::CloseCurrentPopup();
382 }
383
384 ImGui::EndPopup();
385 }
386
387 // All editor windows are now rendered in Update() using either EditorPanel
388 // system or MapPropertiesSystem for map-specific panels. This keeps the
389 // toolset clean and prevents ImGui ID stack issues.
390
391 // Legacy window code removed - windows rendered in Update() include:
392 // - Graphics Groups (EditorPanel)
393 // - Area Configuration (MapPropertiesSystem)
394 // - Background Color Editor (MapPropertiesSystem)
395 // - Visual Effects Editor (MapPropertiesSystem)
396 // - Tile16 Editor, Usage Stats, etc. (EditorPanels)
397
398 // Handle keyboard shortcuts (centralized in dedicated method)
400
401 return absl::OkStatus();
402}
403
405 // Skip processing if any ImGui item is active (e.g., text input)
406 if (ImGui::IsAnyItemActive()) {
407 return;
408 }
409
410 using enum EditingMode;
411
412 // Track mode changes for canvas usage mode updates
413 EditingMode old_mode = current_mode;
414
415 // Tool shortcuts (1-2 for mode selection)
416 if (ImGui::IsKeyDown(ImGuiKey_1)) {
418 } else if (ImGui::IsKeyDown(ImGuiKey_2)) {
420 }
421
422 // Update canvas usage mode when mode changes
423 if (old_mode != current_mode) {
426 } else if (current_mode == EditingMode::DRAW_TILE) {
428 }
429 }
430
431 // Entity editing shortcuts (3-8)
433
434 // View shortcuts
435 if (ImGui::IsKeyDown(ImGuiKey_F11)) {
437 }
438
439 // Toggle map lock with Ctrl+L
440 if (ImGui::IsKeyDown(ImGuiKey_L) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
442 }
443
444 // Toggle Tile16 editor with Ctrl+T
445 if (ImGui::IsKeyDown(ImGuiKey_T) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
449 }
450 }
451
452 // Undo/Redo shortcuts
454}
455
484
486 // Check for Ctrl key (either left or right)
487 bool ctrl_held = ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
488 ImGui::IsKeyDown(ImGuiKey_RightCtrl);
489 if (!ctrl_held) {
490 return;
491 }
492
493 // Ctrl+Z: Undo (or Ctrl+Shift+Z: Redo)
494 if (ImGui::IsKeyPressed(ImGuiKey_Z, false)) {
495 bool shift_held = ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
496 ImGui::IsKeyDown(ImGuiKey_RightShift);
497 if (shift_held) {
498 status_ = Redo(); // Ctrl+Shift+Z = Redo
499 } else {
500 status_ = Undo(); // Ctrl+Z = Undo
501 }
502 }
503
504 // Ctrl+Y: Redo (Windows style)
505 if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) {
506 status_ = Redo();
507 }
508}
509
511 // Get hovered entity from previous frame's rendering pass
512 zelda3::GameEntity* hovered_entity =
513 entity_renderer_ ? entity_renderer_->hovered_entity() : nullptr;
514
515 // Handle all MOUSE mode interactions here
517 HandleEntityContextMenus(hovered_entity);
518 HandleEntityDoubleClick(hovered_entity);
519 }
520
521 // Process any pending entity insertion from context menu
522 // This must be called outside the context menu popup context for OpenPopup
523 // to work
525
526 // Draw entity editor popups and update entity data
528}
529
531 zelda3::GameEntity* hovered_entity) {
532 if (!ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
533 return;
534 }
535
536 if (!hovered_entity) {
537 return;
538 }
539
540 current_entity_ = hovered_entity;
541 switch (hovered_entity->entity_type_) {
543 current_exit_ = *static_cast<zelda3::OverworldExit*>(hovered_entity);
544 ImGui::OpenPopup(
546 .c_str());
547 break;
550 *static_cast<zelda3::OverworldEntrance*>(hovered_entity);
551 ImGui::OpenPopup(
553 .c_str());
554 break;
556 current_item_ = *static_cast<zelda3::OverworldItem*>(hovered_entity);
557 ImGui::OpenPopup(
559 .c_str());
560 break;
562 current_sprite_ = *static_cast<zelda3::Sprite*>(hovered_entity);
563 ImGui::OpenPopup(
565 .c_str());
566 break;
567 default:
568 break;
569 }
570}
571
573 zelda3::GameEntity* hovered_entity) {
574 if (!hovered_entity || !ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
575 return;
576 }
577
580 static_cast<zelda3::OverworldExit*>(hovered_entity)->room_id_;
581 } else if (hovered_entity->entity_type_ ==
584 static_cast<zelda3::OverworldEntrance*>(hovered_entity)->entrance_id_;
585 }
586}
587
619
621 // Get the current zoom scale for positioning and sizing
622 float scale = ow_map_canvas_.global_scale();
623 if (scale <= 0.0f)
624 scale = 1.0f;
625
626 int xx = 0;
627 int yy = 0;
628 for (int i = 0; i < 0x40; i++) {
629 int world_index = i + (current_world_ * 0x40);
630
631 // Bounds checking to prevent crashes
632 if (world_index < 0 || world_index >= static_cast<int>(maps_bmp_.size())) {
633 continue; // Skip invalid map index
634 }
635
636 // Apply scale to positions for proper zoom support
637 int map_x = static_cast<int>(xx * kOverworldMapSize * scale);
638 int map_y = static_cast<int>(yy * kOverworldMapSize * scale);
639
640 // Check if the map has a texture, if not, ensure it gets loaded
641 if (!maps_bmp_[world_index].texture() &&
642 maps_bmp_[world_index].is_active()) {
643 EnsureMapTexture(world_index);
644 }
645
646 // Only draw if the map has a valid texture AND is active (has bitmap data)
647 // The current_map_ check was causing crashes when hovering over unbuilt maps
648 // because the bitmap would be drawn before EnsureMapBuilt() was called
649 bool can_draw =
650 maps_bmp_[world_index].texture() && maps_bmp_[world_index].is_active();
651
652 if (can_draw) {
653 // Draw bitmap at scaled position with scale applied to size
654 ow_map_canvas_.DrawBitmap(maps_bmp_[world_index], map_x, map_y, scale);
655 } else {
656 // Draw a placeholder for maps that haven't loaded yet
657 ImDrawList* draw_list = ImGui::GetWindowDrawList();
658 ImVec2 canvas_pos = ow_map_canvas_.zero_point();
659 ImVec2 scrolling = ow_map_canvas_.scrolling();
660 // Apply scrolling offset and use already-scaled map_x/map_y
661 ImVec2 placeholder_pos = ImVec2(canvas_pos.x + scrolling.x + map_x,
662 canvas_pos.y + scrolling.y + map_y);
663 // Scale the placeholder size to match zoomed maps
664 float scaled_size = kOverworldMapSize * scale;
665 ImVec2 placeholder_size = ImVec2(scaled_size, scaled_size);
666
667 // Modern loading indicator with theme colors
668 draw_list->AddRectFilled(
669 placeholder_pos,
670 ImVec2(placeholder_pos.x + placeholder_size.x,
671 placeholder_pos.y + placeholder_size.y),
672 IM_COL32(32, 32, 32, 128)); // Dark gray with transparency
673
674 // Animated loading spinner - scale spinner radius with zoom
675 ImVec2 spinner_pos = ImVec2(placeholder_pos.x + placeholder_size.x / 2,
676 placeholder_pos.y + placeholder_size.y / 2);
677
678 const float spinner_radius = 8.0f * scale;
679 const float rotation = static_cast<float>(ImGui::GetTime()) * 3.0f;
680 const float start_angle = rotation;
681 const float end_angle = rotation + IM_PI * 1.5f;
682
683 draw_list->PathArcTo(spinner_pos, spinner_radius, start_angle, end_angle,
684 12);
685 draw_list->PathStroke(IM_COL32(100, 180, 100, 255), 0, 2.5f * scale);
686 }
687
688 xx++;
689 if (xx >= 8) {
690 yy++;
691 xx = 0;
692 }
693 }
694}
695
697 // Determine which overworld map the user is currently editing.
698 // drawn_tile_position() returns scaled coordinates, need to unscale
699 auto scaled_position = ow_map_canvas_.drawn_tile_position();
700 float scale = ow_map_canvas_.global_scale();
701 if (scale <= 0.0f)
702 scale = 1.0f;
703
704 // Convert scaled position to world coordinates
705 ImVec2 mouse_position =
706 ImVec2(scaled_position.x / scale, scaled_position.y / scale);
707
708 int map_x = static_cast<int>(mouse_position.x) / kOverworldMapSize;
709 int map_y = static_cast<int>(mouse_position.y) / kOverworldMapSize;
710 current_map_ = map_x + map_y * 8;
711 if (current_world_ == 1) {
712 current_map_ += 0x40;
713 } else if (current_world_ == 2) {
714 current_map_ += 0x80;
715 }
716
717 // Bounds checking to prevent crashes
718 if (current_map_ < 0 || current_map_ >= static_cast<int>(maps_bmp_.size())) {
719 return; // Invalid map index, skip drawing
720 }
721
722 // Validate tile16_blockset_ before calling GetTilemapData
724 tile16_blockset_.atlas.vector().empty()) {
725 LOG_ERROR(
726 "OverworldEditor",
727 "Error: tile16_blockset_ is not properly initialized (active: %s, "
728 "size: %zu)",
729 tile16_blockset_.atlas.is_active() ? "true" : "false",
730 tile16_blockset_.atlas.vector().size());
731 return; // Skip drawing if blockset is invalid
732 }
733
734 // Render the updated map bitmap.
736 RenderUpdatedMapBitmap(mouse_position, tile_data);
737
738 // Calculate the correct superX and superY values
739 int superY = current_map_ / 8;
740 int superX = current_map_ % 8;
741 int mouse_x = static_cast<int>(mouse_position.x);
742 int mouse_y = static_cast<int>(mouse_position.y);
743 // Calculate the correct tile16_x and tile16_y positions
744 int tile16_x = (mouse_x % kOverworldMapSize) / (kOverworldMapSize / 32);
745 int tile16_y = (mouse_y % kOverworldMapSize) / (kOverworldMapSize / 32);
746
747 // Update the overworld_.map_tiles() based on tile16 ID and current world
748 auto& selected_world =
749 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
750 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
751 : overworld_.mutable_map_tiles()->special_world;
752
753 int index_x = superX * 32 + tile16_x;
754 int index_y = superY * 32 + tile16_y;
755
756 // Get old tile value for undo tracking
757 int old_tile_id = selected_world[index_x][index_y];
758
759 // Only record undo if tile is actually changing
760 if (old_tile_id != current_tile16_) {
762 old_tile_id);
763 }
764
765 selected_world[index_x][index_y] = current_tile16_;
766}
767
769 const ImVec2& click_position, const std::vector<uint8_t>& tile_data) {
770 // Bounds checking to prevent crashes
771 if (current_map_ < 0 || current_map_ >= static_cast<int>(maps_bmp_.size())) {
772 LOG_ERROR("OverworldEditor",
773 "ERROR: RenderUpdatedMapBitmap - Invalid current_map_ %d "
774 "(maps_bmp_.size()=%zu)",
775 current_map_, maps_bmp_.size());
776 return; // Invalid map index, skip rendering
777 }
778
779 // Calculate the tile index for x and y based on the click_position
780 int tile_index_x =
781 (static_cast<int>(click_position.x) % kOverworldMapSize) / kTile16Size;
782 int tile_index_y =
783 (static_cast<int>(click_position.y) % kOverworldMapSize) / kTile16Size;
784
785 // Calculate the pixel start position based on tile index and tile size
786 ImVec2 start_position;
787 start_position.x = static_cast<float>(tile_index_x * kTile16Size);
788 start_position.y = static_cast<float>(tile_index_y * kTile16Size);
789
790 // Update the bitmap's pixel data based on the start_position and tile_data
791 gfx::Bitmap& current_bitmap = maps_bmp_[current_map_];
792
793 // Validate bitmap state before writing
794 if (!current_bitmap.is_active() || current_bitmap.size() == 0) {
795 LOG_ERROR(
796 "OverworldEditor",
797 "ERROR: RenderUpdatedMapBitmap - Bitmap %d is not active or has no "
798 "data (active=%s, size=%zu)",
799 current_map_, current_bitmap.is_active() ? "true" : "false",
800 current_bitmap.size());
801 return;
802 }
803
804 for (int y = 0; y < kTile16Size; ++y) {
805 for (int x = 0; x < kTile16Size; ++x) {
806 int pixel_index =
807 (start_position.y + y) * kOverworldMapSize + (start_position.x + x);
808
809 // Bounds check for pixel index
810 if (pixel_index < 0 ||
811 pixel_index >= static_cast<int>(current_bitmap.size())) {
812 LOG_ERROR(
813 "OverworldEditor",
814 "ERROR: RenderUpdatedMapBitmap - pixel_index %d out of bounds "
815 "(bitmap size=%zu)",
816 pixel_index, current_bitmap.size());
817 continue;
818 }
819
820 // Bounds check for tile data
821 int tile_data_index = y * kTile16Size + x;
822 if (tile_data_index < 0 ||
823 tile_data_index >= static_cast<int>(tile_data.size())) {
824 LOG_ERROR(
825 "OverworldEditor",
826 "ERROR: RenderUpdatedMapBitmap - tile_data_index %d out of bounds "
827 "(tile_data size=%zu)",
828 tile_data_index, tile_data.size());
829 continue;
830 }
831
832 current_bitmap.WriteToPixel(pixel_index, tile_data[tile_data_index]);
833 }
834 }
835
836 current_bitmap.set_modified(true);
837
838 // Immediately update the texture to reflect changes
840 &current_bitmap);
841}
842
844 LOG_DEBUG("OverworldEditor", "CheckForOverworldEdits: Frame %d",
845 ImGui::GetFrameCount());
846
848
849 // User has selected a tile they want to draw from the blockset
850 // and clicked on the canvas.
851 // Note: With TileSelectorWidget, we check if a valid tile is selected instead
852 // of canvas points
856 }
857
859 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
860 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
861 LOG_DEBUG("OverworldEditor",
862 "CheckForOverworldEdits: About to apply rectangle selection");
863
864 auto& selected_world =
865 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
866 : (current_world_ == 1)
867 ? overworld_.mutable_map_tiles()->dark_world
868 : overworld_.mutable_map_tiles()->special_world;
869 // selected_points are now stored in world coordinates
870 auto start = ow_map_canvas_.selected_points()[0];
871 auto end = ow_map_canvas_.selected_points()[1];
872
873 // Calculate the bounds of the rectangle in terms of 16x16 tile indices
874 int start_x = std::floor(start.x / kTile16Size) * kTile16Size;
875 int start_y = std::floor(start.y / kTile16Size) * kTile16Size;
876 int end_x = std::floor(end.x / kTile16Size) * kTile16Size;
877 int end_y = std::floor(end.y / kTile16Size) * kTile16Size;
878
879 if (start_x > end_x)
880 std::swap(start_x, end_x);
881 if (start_y > end_y)
882 std::swap(start_y, end_y);
883
884 constexpr int local_map_size = 512; // Size of each local map
885 // Number of tiles per local map (since each tile is 16x16)
886 constexpr int tiles_per_local_map = local_map_size / kTile16Size;
887
888 LOG_DEBUG("OverworldEditor",
889 "CheckForOverworldEdits: About to fill rectangle with "
890 "current_tile16_=%d",
892
893 // Apply the selected tiles to each position in the rectangle
894 // CRITICAL FIX: Use pre-computed tile16_ids_ instead of recalculating
895 // from selected_tiles_ This prevents wrapping issues when dragging near
896 // boundaries
897 int i = 0;
898 for (int y = start_y;
899 y <= end_y && i < static_cast<int>(selected_tile16_ids_.size());
900 y += kTile16Size) {
901 for (int x = start_x;
902 x <= end_x && i < static_cast<int>(selected_tile16_ids_.size());
903 x += kTile16Size, ++i) {
904 // Determine which local map (512x512) the tile is in
905 int local_map_x = x / local_map_size;
906 int local_map_y = y / local_map_size;
907
908 // Calculate the tile's position within its local map
909 int tile16_x = (x % local_map_size) / kTile16Size;
910 int tile16_y = (y % local_map_size) / kTile16Size;
911
912 // Calculate the index within the overall map structure
913 int index_x = local_map_x * tiles_per_local_map + tile16_x;
914 int index_y = local_map_y * tiles_per_local_map + tile16_y;
915
916 // FIXED: Use pre-computed tile ID from the ORIGINAL selection
917 int tile16_id = selected_tile16_ids_[i];
918 // Bounds check for the selected world array, accounting for rectangle
919 // size Ensure the entire rectangle fits within the world bounds
920 int rect_width = ((end_x - start_x) / kTile16Size) + 1;
921 int rect_height = ((end_y - start_y) / kTile16Size) + 1;
922
923 // Prevent painting from wrapping around at the edges of large maps
924 // Only allow painting if the entire rectangle is within the same
925 // 512x512 local map
926 int start_local_map_x = start_x / local_map_size;
927 int start_local_map_y = start_y / local_map_size;
928 int end_local_map_x = end_x / local_map_size;
929 int end_local_map_y = end_y / local_map_size;
930
931 bool in_same_local_map = (start_local_map_x == end_local_map_x) &&
932 (start_local_map_y == end_local_map_y);
933
934 if (in_same_local_map && index_x >= 0 &&
935 (index_x + rect_width - 1) < 0x200 && index_y >= 0 &&
936 (index_y + rect_height - 1) < 0x200) {
937 // Get old tile value for undo tracking
938 int old_tile_id = selected_world[index_x][index_y];
939 if (old_tile_id != tile16_id) {
941 old_tile_id);
942 }
943
944 selected_world[index_x][index_y] = tile16_id;
945
946 // CRITICAL FIX: Also update the bitmap directly like single tile
947 // drawing
948 ImVec2 tile_position(x, y);
949 auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile16_id);
950 if (!tile_data.empty()) {
951 RenderUpdatedMapBitmap(tile_position, tile_data);
952 LOG_DEBUG(
953 "OverworldEditor",
954 "CheckForOverworldEdits: Updated bitmap at position (%d,%d) "
955 "with tile16_id=%d",
956 x, y, tile16_id);
957 } else {
958 LOG_ERROR("OverworldEditor",
959 "ERROR: Failed to get tile data for tile16_id=%d",
960 tile16_id);
961 }
962 }
963 }
964 }
965
966 // Finalize the undo batch operation after all tiles are placed
968
970 // Clear the rectangle selection after applying
971 // This is commented out for now, will come back to later.
972 // ow_map_canvas_.mutable_selected_tiles()->clear();
973 // ow_map_canvas_.mutable_points()->clear();
974 LOG_DEBUG(
975 "OverworldEditor",
976 "CheckForOverworldEdits: Rectangle selection applied and cleared");
977 }
978 }
979}
980
982 // Pass the canvas scale for proper zoom handling
983 float scale = ow_map_canvas_.global_scale();
984 if (scale <= 0.0f)
985 scale = 1.0f;
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
1019 return absl::FailedPreconditionError("Clipboard unavailable");
1020 }
1021 // If a rectangle selection exists, copy its tile16 IDs into shared clipboard
1023 !ow_map_canvas_.selected_points().empty()) {
1024 std::vector<int> ids;
1025 // selected_points are now stored in world coordinates
1026 const auto start = ow_map_canvas_.selected_points()[0];
1027 const auto end = ow_map_canvas_.selected_points()[1];
1028 const int start_x =
1029 static_cast<int>(std::floor(std::min(start.x, end.x) / 16.0f));
1030 const int end_x =
1031 static_cast<int>(std::floor(std::max(start.x, end.x) / 16.0f));
1032 const int start_y =
1033 static_cast<int>(std::floor(std::min(start.y, end.y) / 16.0f));
1034 const int end_y =
1035 static_cast<int>(std::floor(std::max(start.y, end.y) / 16.0f));
1036 const int width = end_x - start_x + 1;
1037 const int height = end_y - start_y + 1;
1038 ids.reserve(width * height);
1041 for (int y = start_y; y <= end_y; ++y) {
1042 for (int x = start_x; x <= end_x; ++x) {
1043 ids.push_back(overworld_.GetTile(x, y));
1044 }
1045 }
1046
1051 return absl::OkStatus();
1052 }
1053 // Single tile copy fallback
1054 if (current_tile16_ >= 0) {
1059 return absl::OkStatus();
1060 }
1061 return absl::FailedPreconditionError("Nothing selected to copy");
1062}
1063
1066 return absl::FailedPreconditionError("Clipboard unavailable");
1067 }
1069 return absl::FailedPreconditionError("Clipboard empty");
1070 }
1071 if (ow_map_canvas_.points().empty() &&
1073 return absl::FailedPreconditionError("No paste target");
1074 }
1075
1076 // Determine paste anchor position (use current mouse drawn tile position)
1077 // Unscale coordinates to get world position
1078 const ImVec2 scaled_anchor = ow_map_canvas_.drawn_tile_position();
1079 float scale = ow_map_canvas_.global_scale();
1080 if (scale <= 0.0f)
1081 scale = 1.0f;
1082 const ImVec2 anchor =
1083 ImVec2(scaled_anchor.x / scale, scaled_anchor.y / scale);
1084
1085 // Compute anchor in tile16 grid within the current map
1086 const int tile16_x =
1087 (static_cast<int>(anchor.x) % kOverworldMapSize) / kTile16Size;
1088 const int tile16_y =
1089 (static_cast<int>(anchor.y) % kOverworldMapSize) / kTile16Size;
1090
1091 auto& selected_world =
1092 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
1093 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
1094 : overworld_.mutable_map_tiles()->special_world;
1095
1096 const int superY = current_map_ / 8;
1097 const int superX = current_map_ % 8;
1098 const int tiles_per_local_map = 512 / kTile16Size;
1099
1103
1104 // Guard
1105 if (width * height != static_cast<int>(ids.size())) {
1106 return absl::InternalError("Clipboard dimensions mismatch");
1107 }
1108
1109 for (int dy = 0; dy < height; ++dy) {
1110 for (int dx = 0; dx < width; ++dx) {
1111 const int id = ids[dy * width + dx];
1112 const int gx = tile16_x + dx;
1113 const int gy = tile16_y + dy;
1114
1115 const int global_x = superX * 32 + gx;
1116 const int global_y = superY * 32 + gy;
1117 if (global_x < 0 || global_x >= 256 || global_y < 0 || global_y >= 256)
1118 continue;
1119 selected_world[global_x][global_y] = id;
1120 }
1121 }
1122
1124 return absl::OkStatus();
1125}
1126
1128 // 4096x4096, 512x512 maps and some are larges maps 1024x1024
1129 // hover_mouse_pos() returns canvas-local coordinates but they're scaled
1130 // Unscale to get world coordinates for map detection
1131 const auto scaled_position = ow_map_canvas_.hover_mouse_pos();
1132 float scale = ow_map_canvas_.global_scale();
1133 if (scale <= 0.0f)
1134 scale = 1.0f;
1135 const int large_map_size = 1024;
1136
1137 // Calculate which small map the mouse is currently over
1138 // Unscale coordinates to get world position
1139 int map_x = static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
1140 int map_y = static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
1141
1142 // Bounds check to prevent out-of-bounds access
1143 if (map_x < 0 || map_x >= 8 || map_y < 0 || map_y >= 8) {
1144 return absl::OkStatus();
1145 }
1146
1147 const bool allow_special_tail =
1149 if (!allow_special_tail && current_world_ == 2 && map_y >= 4) {
1150 // Special world is only 4 rows high unless expansion is enabled
1151 return absl::OkStatus();
1152 }
1153
1154 // Calculate the index of the map in the `maps_bmp_` vector
1155 int hovered_map = map_x + map_y * 8;
1156 if (current_world_ == 1) {
1157 hovered_map += 0x40;
1158 } else if (current_world_ == 2) {
1159 hovered_map += 0x80;
1160 }
1161
1162 // Only update current_map_ if not locked
1163 if (!current_map_lock_) {
1164 current_map_ = hovered_map;
1166
1167 // Hover debouncing: Only build expensive maps after dwelling on them
1168 // This prevents lag when rapidly moving mouse across the overworld
1169 bool should_build = false;
1170 if (hovered_map != last_hovered_map_) {
1171 // New map hovered - reset timer
1172 last_hovered_map_ = hovered_map;
1173 hover_time_ = 0.0f;
1174 // Check if already built (instant display)
1175 should_build = overworld_.overworld_map(hovered_map)->is_built();
1176 } else {
1177 // Same map - accumulate hover time
1178 hover_time_ += ImGui::GetIO().DeltaTime;
1179 // Build after delay OR if clicking
1180 should_build = (hover_time_ >= kHoverBuildDelay) ||
1181 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
1182 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
1183 }
1184
1185 // Only trigger expensive build if debounce threshold met
1186 if (should_build) {
1188 }
1189
1190 // After dwelling longer, start pre-loading adjacent maps
1191 if (hover_time_ >= kPreloadStartDelay && preload_queue_.empty()) {
1193 }
1194
1195 // Process one preload per frame (background optimization)
1197 }
1198
1199 const int current_highlighted_map = current_map_;
1200
1201 // Use centralized version detection
1203 bool use_v3_area_sizes =
1205
1206 // Get area size for v3+ ROMs, otherwise use legacy logic
1207 if (use_v3_area_sizes) {
1209 auto area_size = overworld_.overworld_map(current_map_)->area_size();
1210 const int highlight_parent =
1211 overworld_.overworld_map(current_highlighted_map)->parent();
1212
1213 // Calculate parent map coordinates accounting for world offset
1214 int parent_map_x;
1215 int parent_map_y;
1216 if (current_world_ == 0) {
1217 // Light World (0x00-0x3F)
1218 parent_map_x = highlight_parent % 8;
1219 parent_map_y = highlight_parent / 8;
1220 } else if (current_world_ == 1) {
1221 // Dark World (0x40-0x7F)
1222 parent_map_x = (highlight_parent - 0x40) % 8;
1223 parent_map_y = (highlight_parent - 0x40) / 8;
1224 } else {
1225 // Special World (0x80-0x9F)
1226 parent_map_x = (highlight_parent - 0x80) % 8;
1227 parent_map_y = (highlight_parent - 0x80) / 8;
1228 }
1229
1230 // Draw outline based on area size
1231 switch (area_size) {
1232 case AreaSizeEnum::LargeArea:
1233 // 2x2 grid (1024x1024)
1235 parent_map_y * kOverworldMapSize,
1236 large_map_size, large_map_size);
1237 break;
1238 case AreaSizeEnum::WideArea:
1239 // 2x1 grid (1024x512) - horizontal
1241 parent_map_y * kOverworldMapSize,
1242 large_map_size, kOverworldMapSize);
1243 break;
1244 case AreaSizeEnum::TallArea:
1245 // 1x2 grid (512x1024) - vertical
1247 parent_map_y * kOverworldMapSize,
1248 kOverworldMapSize, large_map_size);
1249 break;
1250 case AreaSizeEnum::SmallArea:
1251 default:
1253 parent_map_y * kOverworldMapSize,
1255 break;
1256 }
1257 } else {
1258 // Legacy logic for vanilla and v2 ROMs
1259 int world_offset = current_world_ * 0x40;
1260 if (overworld_.overworld_map(current_map_)->is_large_map() ||
1261 overworld_.overworld_map(current_map_)->large_index() != 0) {
1262 const int highlight_parent =
1263 overworld_.overworld_map(current_highlighted_map)->parent();
1264
1265 // CRITICAL FIX: Account for world offset when calculating parent
1266 // coordinates For Dark World (0x40-0x7F), parent IDs are in range
1267 // 0x40-0x7F For Special World (0x80-0x9F), parent IDs are in range
1268 // 0x80-0x9F We need to subtract the world offset to get display grid
1269 // coordinates (0-7)
1270 int parent_map_x;
1271 int parent_map_y;
1272 if (current_world_ == 0) {
1273 // Light World (0x00-0x3F)
1274 parent_map_x = highlight_parent % 8;
1275 parent_map_y = highlight_parent / 8;
1276 } else if (current_world_ == 1) {
1277 // Dark World (0x40-0x7F) - subtract 0x40 to get display coordinates
1278 parent_map_x = (highlight_parent - 0x40) % 8;
1279 parent_map_y = (highlight_parent - 0x40) / 8;
1280 } else {
1281 // Special World (0x80-0x9F) - subtract 0x80 to get display coordinates
1282 parent_map_x = (highlight_parent - 0x80) % 8;
1283 parent_map_y = (highlight_parent - 0x80) / 8;
1284 }
1285
1287 parent_map_y * kOverworldMapSize,
1288 large_map_size, large_map_size);
1289 } else {
1290 // Calculate map coordinates accounting for world offset
1291 int current_map_x;
1292 int current_map_y;
1293 if (current_world_ == 0) {
1294 // Light World (0x00-0x3F)
1295 current_map_x = current_highlighted_map % 8;
1296 current_map_y = current_highlighted_map / 8;
1297 } else if (current_world_ == 1) {
1298 // Dark World (0x40-0x7F)
1299 current_map_x = (current_highlighted_map - 0x40) % 8;
1300 current_map_y = (current_highlighted_map - 0x40) / 8;
1301 } else {
1302 // Special World (0x80-0x9F) - use display coordinates based on
1303 // current_world_ The special world maps are displayed in the same 8x8
1304 // grid as LW/DW
1305 current_map_x = (current_highlighted_map - 0x80) % 8;
1306 current_map_y = (current_highlighted_map - 0x80) / 8;
1307 }
1309 current_map_y * kOverworldMapSize,
1311 }
1312 }
1313
1314 // Ensure current map has texture created for rendering
1316
1317 if (maps_bmp_[current_map_].modified()) {
1320
1321 // Ensure tile16 blockset is fully updated before rendering
1325 }
1326
1327 // Update map texture with the traditional direct update approach
1330 maps_bmp_[current_map_].set_modified(false);
1331 }
1332
1333 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
1335 }
1336
1337 // If double clicked, toggle the current map
1338 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
1340 }
1341
1342 return absl::OkStatus();
1343}
1344
1345// Overworld Canvas Pan/Zoom Helpers
1346
1347namespace {
1348
1349// Calculate the total canvas content size based on world layout
1351 // 8x8 grid of 512x512 maps = 4096x4096 total
1352 constexpr float kWorldSize = 512.0f * 8.0f; // 4096
1353 return ImVec2(kWorldSize * scale, kWorldSize * scale);
1354}
1355
1356// Clamp scroll position to valid bounds
1357ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size,
1358 ImVec2 visible_size) {
1359 // Calculate maximum scroll values
1360 float max_scroll_x = std::max(0.0f, content_size.x - visible_size.x);
1361 float max_scroll_y = std::max(0.0f, content_size.y - visible_size.y);
1362
1363 // Clamp to valid range [min_scroll, 0]
1364 // Note: Canvas uses negative scrolling for right/down
1365 float clamped_x = std::clamp(scroll.x, -max_scroll_x, 0.0f);
1366 float clamped_y = std::clamp(scroll.y, -max_scroll_y, 0.0f);
1367
1368 return ImVec2(clamped_x, clamped_y);
1369}
1370
1371} // namespace
1372
1374 // Legacy wrapper - now calls HandleOverworldPan
1376}
1377
1379 // Simplified map settings - compact row with popup panels for detailed
1380 // editing
1382 bool has_selection = ow_map_canvas_.select_rect_active() &&
1383 !ow_map_canvas_.selected_tiles().empty();
1384
1385 // Check if scratch space has data
1386 bool scratch_has_data = scratch_space_.in_use;
1387
1388 // Pass PanelManager to toolbar for panel visibility management
1391 has_selection, scratch_has_data, rom_, &overworld_);
1392 }
1393
1394 // ==========================================================================
1395 // PHASE 3: Modern BeginCanvas/EndCanvas Pattern
1396 // ==========================================================================
1397 // Context menu setup MUST happen BEFORE BeginCanvas (lesson from dungeon)
1398 bool show_context_menu =
1400 (!entity_renderer_ || entity_renderer_->hovered_entity() == nullptr);
1401
1404 map_properties_system_->SetupCanvasContextMenu(
1407 show_overlay_editor_, static_cast<int>(current_mode));
1408 }
1409
1410 // Configure canvas frame options
1411 gui::CanvasFrameOptions frame_opts;
1412 frame_opts.canvas_size = kOverworldCanvasSize;
1413 frame_opts.draw_grid = true;
1414 frame_opts.grid_step = 64.0f; // Map boundaries (512px / 8 maps)
1415 frame_opts.draw_context_menu = show_context_menu;
1416 frame_opts.draw_overlay = true;
1417 frame_opts.render_popups = true;
1418 frame_opts.use_child_window = false; // CRITICAL: Canvas has own pan logic
1419
1420 // Wrap in child window for scrollbars
1423
1424 // Keep canvas scroll at 0 - ImGui's child window handles all scrolling
1425 // The scrollbars scroll the child window which moves the entire canvas
1426 ow_map_canvas_.set_scrolling(ImVec2(0, 0));
1427
1428 // Begin canvas frame - this handles DrawBackground + DrawContextMenu
1429 auto canvas_rt = gui::BeginCanvas(ow_map_canvas_, frame_opts);
1431
1432 // Handle pan via ImGui scrolling (instead of canvas internal scroll)
1435
1436 // Tile painting mode - handle tile edits and right-click tile picking
1439 }
1440
1441 if (overworld_.is_loaded()) {
1442 // Draw the 64 overworld map bitmaps
1444
1445 // Draw all entities using the new CanvasRuntime-based methods
1446 if (entity_renderer_) {
1447 entity_renderer_->DrawExits(canvas_rt, current_world_);
1448 entity_renderer_->DrawEntrances(canvas_rt, current_world_);
1449 entity_renderer_->DrawItems(canvas_rt, current_world_);
1450 entity_renderer_->DrawSprites(canvas_rt, current_world_, game_state_);
1451 }
1452
1453 // Draw overlay preview if enabled
1455 map_properties_system_->DrawOverlayPreviewOnMap(
1457 }
1458
1461 }
1462
1463 // Use canvas runtime hover state for map detection
1464 if (canvas_rt.hovered) {
1466 }
1467
1468 // --- BEGIN ENTITY DRAG/DROP LOGIC ---
1470 auto hovered_entity = entity_renderer_->hovered_entity();
1471
1472 // 1. Initiate drag
1473 if (!is_dragging_entity_ && hovered_entity &&
1474 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1475 dragged_entity_ = hovered_entity;
1476 is_dragging_entity_ = true;
1480 }
1481 }
1482
1483 // 2. Update drag
1485 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
1486 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1487 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
1488 float scale = canvas_rt.scale;
1489 if (scale > 0.0f) {
1490 dragged_entity_->x_ += mouse_delta.x / scale;
1491 dragged_entity_->y_ += mouse_delta.y / scale;
1492 }
1493 }
1494
1495 // 3. End drag
1496 if (is_dragging_entity_ &&
1497 ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
1498 if (dragged_entity_) {
1499 float end_scale = canvas_rt.scale;
1500 MoveEntityOnGrid(dragged_entity_, canvas_rt.canvas_p0,
1501 canvas_rt.scrolling, dragged_entity_free_movement_,
1502 end_scale);
1503 // Pass overworld context for proper area size detection
1505 &overworld_);
1506 rom_->set_dirty(true);
1507 }
1508 is_dragging_entity_ = false;
1509 dragged_entity_ = nullptr;
1511 }
1512 }
1513 // --- END ENTITY DRAG/DROP LOGIC ---
1514 }
1515
1516 // End canvas frame - draws grid/overlay based on frame_opts
1517 gui::EndCanvas(ow_map_canvas_, canvas_rt, frame_opts);
1518 ImGui::EndChild();
1519}
1520
1523 ImGui::BeginGroup();
1524 gui::BeginChildWithScrollbar("##Tile16SelectorScrollRegion");
1526
1527 if (!blockset_selector_) {
1528 gui::TileSelectorWidget::Config selector_config;
1529 selector_config.tile_size = 16;
1530 selector_config.display_scale = 2.0f;
1531 selector_config.tiles_per_row = 8;
1532 selector_config.total_tiles = zelda3::kNumTile16Individual;
1533 selector_config.draw_offset = ImVec2(2.0f, 0.0f);
1534 selector_config.highlight_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f);
1535
1536 blockset_selector_ = std::make_unique<gui::TileSelectorWidget>(
1537 "OwBlocksetSelector", selector_config);
1538 blockset_selector_->AttachCanvas(&blockset_canvas_);
1539 }
1540
1542
1544 bool atlas_ready = map_blockset_loaded_ && atlas.is_active();
1545 auto result = blockset_selector_->Render(atlas, atlas_ready);
1546
1547 if (result.selection_changed) {
1548 current_tile16_ = result.selected_tile;
1549 // Set the current tile in the editor (original behavior)
1551 if (!status.ok()) {
1552 util::logf("Failed to set tile16: %s", status.message().data());
1553 }
1554 // Note: We do NOT auto-scroll here because it breaks user interaction.
1555 // The canvas should only scroll when explicitly requested (e.g., when
1556 // selecting a tile from the overworld canvas via
1557 // ScrollBlocksetCanvasToCurrentTile).
1558 }
1559
1560 if (result.tile_double_clicked) {
1563 }
1564 }
1565
1566 ImGui::EndChild();
1567 ImGui::EndGroup();
1568 return absl::OkStatus();
1569}
1570
1572 // Configure canvas frame options for graphics bin
1573 gui::CanvasFrameOptions frame_opts;
1575 frame_opts.draw_grid = true;
1576 frame_opts.grid_step = 16.0f; // Tile8 grid
1577 frame_opts.draw_context_menu = true;
1578 frame_opts.draw_overlay = true;
1579 frame_opts.render_popups = true;
1580 frame_opts.use_child_window = false;
1581
1582 auto canvas_rt = gui::BeginCanvas(graphics_bin_canvas_, frame_opts);
1583
1584 if (all_gfx_loaded_) {
1585 int key = 0;
1586 for (auto& value : gfx::Arena::Get().gfx_sheets()) {
1587 int offset = 0x40 * (key + 1);
1588 int top_left_y = canvas_rt.canvas_p0.y + 2;
1589 if (key >= 1) {
1590 top_left_y = canvas_rt.canvas_p0.y + 0x40 * key;
1591 }
1592 auto texture = value.texture();
1593 canvas_rt.draw_list->AddImage(
1594 (ImTextureID)(intptr_t)texture,
1595 ImVec2(canvas_rt.canvas_p0.x + 2, top_left_y),
1596 ImVec2(canvas_rt.canvas_p0.x + 0x100,
1597 canvas_rt.canvas_p0.y + offset));
1598 key++;
1599 }
1600 }
1601
1602 gui::EndCanvas(graphics_bin_canvas_, canvas_rt, frame_opts);
1603}
1604
1606 if (map_id < 0) {
1607 // Invalidate all maps - clear both editor cache and Overworld's tileset cache
1608 current_graphics_set_.clear();
1610 } else {
1611 // Invalidate specific map and its siblings in the Overworld's tileset cache
1612 current_graphics_set_.erase(map_id);
1614 }
1615}
1616
1618 if (overworld_.is_loaded()) {
1619 // Always ensure current map graphics are loaded
1620 if (!current_graphics_set_.contains(current_map_)) {
1623 auto bmp = std::make_unique<gfx::Bitmap>();
1624 bmp->Create(0x80, kOverworldMapSize, 0x08, overworld_.current_graphics());
1625 bmp->SetPalette(palette_);
1626 current_graphics_set_[current_map_] = std::move(bmp);
1630 }
1631 }
1632
1633 // Configure canvas frame options for area graphics
1634 gui::CanvasFrameOptions frame_opts;
1636 frame_opts.draw_grid = true;
1637 frame_opts.grid_step = 32.0f; // Tile selector grid
1638 frame_opts.draw_context_menu = true;
1639 frame_opts.draw_overlay = true;
1640 frame_opts.render_popups = true;
1641 frame_opts.use_child_window = false;
1642
1644 ImGui::BeginGroup();
1645 gui::BeginChildWithScrollbar("##AreaGraphicsScrollRegion");
1646
1647 auto canvas_rt = gui::BeginCanvas(current_gfx_canvas_, frame_opts);
1649
1650 if (current_graphics_set_.contains(current_map_) &&
1651 current_graphics_set_[current_map_]->is_active()) {
1653 2.0f);
1654 }
1656
1657 gui::EndCanvas(current_gfx_canvas_, canvas_rt, frame_opts);
1658 ImGui::EndChild();
1659 ImGui::EndGroup();
1660 return absl::OkStatus();
1661}
1662
1664 // Delegate to the existing GfxGroupEditor
1665 if (rom_ && rom_->is_loaded()) {
1666 return gfx_group_editor_.Update();
1667 } else {
1668 gui::CenterText("No ROM loaded");
1669 return absl::OkStatus();
1670 }
1671}
1672
1674 // v3 Settings panel - placeholder for ZSCustomOverworld configuration
1675 ImGui::TextWrapped("ZSCustomOverworld v3 settings panel");
1676 ImGui::Separator();
1677
1678 if (!rom_ || !rom_->is_loaded()) {
1679 gui::CenterText("No ROM loaded");
1680 return;
1681 }
1682
1683 ImGui::TextWrapped(
1684 "This panel will contain ZSCustomOverworld configuration options "
1685 "such as custom map sizes, extended tile sets, and other v3 features.");
1686
1687 // TODO: Implement v3 settings UI
1688 // Could include:
1689 // - Custom map size toggles
1690 // - Extended tileset configuration
1691 // - Override settings
1692 // - Version information display
1693}
1694
1696 // Area Configuration panel
1697 static bool show_custom_bg_color_editor = false;
1698 static bool show_overlay_editor = false;
1699 static int game_state = 0; // 0=Beginning, 1=Zelda Saved, 2=Master Sword
1700
1701 if (sidebar_) {
1703 show_custom_bg_color_editor, show_overlay_editor);
1704 }
1705
1706 // Draw popups if triggered from sidebar
1707 if (show_custom_bg_color_editor) {
1708 ImGui::OpenPopup("CustomBGColorEditor");
1709 show_custom_bg_color_editor = false; // Reset after opening
1710 }
1711 if (show_overlay_editor) {
1712 ImGui::OpenPopup("OverlayEditor");
1713 show_overlay_editor = false; // Reset after opening
1714 }
1715
1716 if (ImGui::BeginPopup("CustomBGColorEditor")) {
1718 map_properties_system_->DrawCustomBackgroundColorEditor(
1719 current_map_, show_custom_bg_color_editor);
1720 }
1721 ImGui::EndPopup();
1722 }
1723
1724 if (ImGui::BeginPopup("OverlayEditor")) {
1726 map_properties_system_->DrawOverlayEditor(current_map_,
1727 show_overlay_editor);
1728 }
1729 ImGui::EndPopup();
1730 }
1731}
1732
1734 if (core::FeatureFlags::get().overworld.kSaveOverworldMaps) {
1739 }
1740 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1742 }
1743 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1745 }
1746 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1748 }
1749 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1752 }
1753 return absl::OkStatus();
1754}
1755
1756// ============================================================================
1757// Undo/Redo System Implementation
1758// ============================================================================
1759
1761 switch (world) {
1762 case 0:
1763 return overworld_.mutable_map_tiles()->light_world;
1764 case 1:
1765 return overworld_.mutable_map_tiles()->dark_world;
1766 default:
1767 return overworld_.mutable_map_tiles()->special_world;
1768 }
1769}
1770
1771void OverworldEditor::CreateUndoPoint(int map_id, int world, int x, int y,
1772 int old_tile_id) {
1773 auto now = std::chrono::steady_clock::now();
1774
1775 // Check if we should batch with current operation (same map, same world,
1776 // within timeout)
1777 if (current_paint_operation_.has_value() &&
1778 current_paint_operation_->map_id == map_id &&
1779 current_paint_operation_->world == world &&
1781 // Add to existing operation
1782 current_paint_operation_->tile_changes.emplace_back(std::make_pair(x, y),
1783 old_tile_id);
1784 } else {
1785 // Finalize any pending operation before starting a new one
1787
1788 // Start new operation
1790 OverworldUndoPoint{.map_id = map_id,
1791 .world = world,
1792 .tile_changes = {{{x, y}, old_tile_id}},
1793 .timestamp = now};
1794 }
1795
1796 last_paint_time_ = now;
1797}
1798
1800 if (!current_paint_operation_.has_value()) {
1801 return;
1802 }
1803
1804 // Clear redo stack when new action is performed
1805 redo_stack_.clear();
1806
1807 // Add to undo stack
1808 undo_stack_.push_back(std::move(*current_paint_operation_));
1810
1811 // Limit stack size
1812 while (undo_stack_.size() > kMaxUndoHistory) {
1813 undo_stack_.erase(undo_stack_.begin());
1814 }
1815}
1816
1818 auto& world_tiles = GetWorldTiles(point.world);
1819
1820 // Apply all tile changes
1821 for (const auto& [coords, tile_id] : point.tile_changes) {
1822 auto [x, y] = coords;
1823 world_tiles[x][y] = tile_id;
1824 }
1825
1826 // Refresh the map visuals
1828}
1829
1831 // Finalize any pending paint operation first
1833
1834 if (undo_stack_.empty()) {
1835 return absl::FailedPreconditionError("Nothing to undo");
1836 }
1837
1838 OverworldUndoPoint point = std::move(undo_stack_.back());
1839 undo_stack_.pop_back();
1840
1841 // Create redo point with current tile values before restoring
1842 OverworldUndoPoint redo_point{.map_id = point.map_id,
1843 .world = point.world,
1844 .tile_changes = {},
1845 .timestamp = std::chrono::steady_clock::now()};
1846
1847 auto& world_tiles = GetWorldTiles(point.world);
1848
1849 // Swap tiles and record for redo
1850 for (const auto& [coords, old_tile_id] : point.tile_changes) {
1851 auto [x, y] = coords;
1852 int current_tile_id = world_tiles[x][y];
1853
1854 // Record current value for redo
1855 redo_point.tile_changes.emplace_back(coords, current_tile_id);
1856
1857 // Restore old value
1858 world_tiles[x][y] = old_tile_id;
1859 }
1860
1861 redo_stack_.push_back(std::move(redo_point));
1862
1863 // Refresh the map visuals
1865
1866 return absl::OkStatus();
1867}
1868
1870 if (redo_stack_.empty()) {
1871 return absl::FailedPreconditionError("Nothing to redo");
1872 }
1873
1874 OverworldUndoPoint point = std::move(redo_stack_.back());
1875 redo_stack_.pop_back();
1876
1877 // Create undo point with current tile values
1878 OverworldUndoPoint undo_point{.map_id = point.map_id,
1879 .world = point.world,
1880 .tile_changes = {},
1881 .timestamp = std::chrono::steady_clock::now()};
1882
1883 auto& world_tiles = GetWorldTiles(point.world);
1884
1885 // Swap tiles and record for undo
1886 for (const auto& [coords, tile_id] : point.tile_changes) {
1887 auto [x, y] = coords;
1888 int current_tile_id = world_tiles[x][y];
1889
1890 // Record current value for undo
1891 undo_point.tile_changes.emplace_back(coords, current_tile_id);
1892
1893 // Apply redo value
1894 world_tiles[x][y] = tile_id;
1895 }
1896
1897 undo_stack_.push_back(std::move(undo_point));
1898
1899 // Refresh the map visuals
1901
1902 return absl::OkStatus();
1903}
1904
1905// ============================================================================
1906
1908 gfx::ScopedTimer timer("LoadGraphics");
1909
1910 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1911 // Load the Link to the Past overworld.
1912 {
1913 gfx::ScopedTimer load_timer("Overworld::Load");
1915 }
1917
1918 // Fix: Set transparency for the first color of each 16-color subpalette
1919 // This ensures the background color (backdrop) shows through
1920 for (size_t i = 0; i < palette_.size(); i += 16) {
1921 if (i < palette_.size()) {
1922 palette_[i].set_transparent(true);
1923 }
1924 }
1925
1926 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1927
1928 // Phase 1: Create bitmaps without textures for faster loading
1929 // This avoids blocking the main thread with GPU texture creation
1930 {
1931 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1937 }
1938
1939 LOG_DEBUG("OverworldEditor",
1940 "Loading overworld tileset (deferred textures).");
1941 {
1942 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1943 tile16_blockset_bmp_.Create(0x80, 0x2000, 0x08,
1948 }
1949 map_blockset_loaded_ = true;
1950
1951 // Copy the tile16 data into individual tiles.
1952 auto tile16_blockset_data = overworld_.tile16_blockset_data();
1953 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
1954
1955 {
1956 gfx::ScopedTimer tilemap_timer("CreateTilemap");
1958 gfx::CreateTilemap(renderer_, tile16_blockset_data, 0x80, 0x2000,
1960
1961 // Queue texture creation for the tile16 blockset atlas
1966 }
1967 }
1968
1969 // Phase 2: Create bitmaps only for essential maps initially
1970 // Non-essential maps will be created on-demand when accessed
1971 // IMPORTANT: Must match kEssentialMapsPerWorld in overworld.cc
1972#ifdef __EMSCRIPTEN__
1973 constexpr int kEssentialMapsPerWorld = 4; // Match WASM build in overworld.cc
1974#else
1975 constexpr int kEssentialMapsPerWorld =
1976 16; // Match native build in overworld.cc
1977#endif
1978 constexpr int kLightWorldEssential = kEssentialMapsPerWorld;
1979 constexpr int kDarkWorldEssential =
1980 zelda3::kDarkWorldMapIdStart + kEssentialMapsPerWorld;
1981 constexpr int kSpecialWorldEssential =
1982 zelda3::kSpecialWorldMapIdStart + kEssentialMapsPerWorld;
1983
1984 LOG_DEBUG(
1985 "OverworldEditor",
1986 "Creating bitmaps for essential maps only (first %d maps per world)",
1987 kEssentialMapsPerWorld);
1988
1989 std::vector<gfx::Bitmap*> maps_to_texture;
1990 maps_to_texture.reserve(kEssentialMapsPerWorld *
1991 3); // 8 maps per world * 3 worlds
1992
1993 {
1994 gfx::ScopedTimer maps_timer("CreateEssentialOverworldMaps");
1995 for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
1996 bool is_essential = false;
1997
1998 // Check if this is an essential map
1999 if (i < kLightWorldEssential) {
2000 is_essential = true;
2001 } else if (i >= zelda3::kDarkWorldMapIdStart && i < kDarkWorldEssential) {
2002 is_essential = true;
2003 } else if (i >= zelda3::kSpecialWorldMapIdStart &&
2004 i < kSpecialWorldEssential) {
2005 is_essential = true;
2006 }
2007
2008 if (is_essential) {
2010 auto palette = overworld_.current_area_palette();
2011 try {
2012 // Create bitmap data and surface but defer texture creation
2015 maps_bmp_[i].SetPalette(palette);
2016 maps_to_texture.push_back(&maps_bmp_[i]);
2017 } catch (const std::bad_alloc& e) {
2018 std::cout << "Error allocating map " << i << ": " << e.what()
2019 << std::endl;
2020 continue;
2021 }
2022 }
2023 // Non-essential maps will be created on-demand when accessed
2024 }
2025 }
2026
2027 // Phase 3: Create textures only for currently visible maps
2028 // Only create textures for the first few maps initially
2029 const int initial_texture_count =
2030 std::min(4, static_cast<int>(maps_to_texture.size()));
2031 {
2032 gfx::ScopedTimer initial_textures_timer("CreateInitialTextures");
2033 for (int i = 0; i < initial_texture_count; ++i) {
2034 // Queue texture creation/update for initial maps via Arena's deferred
2035 // system
2037 gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]);
2038 }
2039 }
2040
2041 // Queue remaining maps for progressive loading via Arena
2042 // Priority based on current world (0 = current world, 11+ = other worlds)
2043 for (size_t i = initial_texture_count; i < maps_to_texture.size(); ++i) {
2044 // Determine priority based on which world this map belongs to
2045 int map_index = -1;
2046 for (int j = 0; j < zelda3::kNumOverworldMaps; ++j) {
2047 if (&maps_bmp_[j] == maps_to_texture[i]) {
2048 map_index = j;
2049 break;
2050 }
2051 }
2052
2053 int priority = 15; // Default low priority
2054 if (map_index >= 0) {
2055 int map_world = map_index / 0x40;
2056 priority = (map_world == current_world_)
2057 ? 5
2058 : 15; // Current world = priority 5, others = 15
2059 }
2060
2061 // Queue texture creation for remaining maps via Arena's deferred system
2062 // Note: Priority system to be implemented in future enhancement
2064 gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]);
2065 }
2066
2067 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
2068 {
2069 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
2071 }
2072 }
2073
2074 return absl::OkStatus();
2075}
2076
2078 // Render the sprites for each Overworld map
2079 const int depth = 0x10;
2080 for (int i = 0; i < 3; i++)
2081 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
2082 int width = sprite.width();
2083 int height = sprite.height();
2084 if (width == 0 || height == 0) {
2085 continue;
2086 }
2087 if (sprite_previews_.size() < sprite.id()) {
2088 sprite_previews_.resize(sprite.id() + 1);
2089 }
2090 sprite_previews_[sprite.id()].Create(width, height, depth,
2091 *sprite.preview_graphics());
2092 sprite_previews_[sprite.id()].SetPalette(palette_);
2095 &sprite_previews_[sprite.id()]);
2096 }
2097 return absl::OkStatus();
2098}
2099
2101 // Process queued texture commands via Arena's deferred system
2102 if (renderer_) {
2104 }
2105
2106 // Also process deferred map refreshes for modified maps
2107 int refresh_count = 0;
2108 const int max_refreshes_per_frame = 2;
2109
2110 for (int i = 0;
2111 i < zelda3::kNumOverworldMaps && refresh_count < max_refreshes_per_frame;
2112 ++i) {
2113 if (maps_bmp_[i].modified() && maps_bmp_[i].is_active()) {
2114 // Check if this map is in current world (prioritize)
2115 bool is_current_world = (i / 0x40 == current_world_);
2116 bool is_current_map = (i == current_map_);
2117
2118 if (is_current_map || is_current_world) {
2120 refresh_count++;
2121 }
2122 }
2123 }
2124}
2125
2127 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
2128 return;
2129 }
2130
2131 // Ensure the map is built first (on-demand loading)
2132 auto status = overworld_.EnsureMapBuilt(map_index);
2133 if (!status.ok()) {
2134 LOG_ERROR("OverworldEditor", "Failed to build map %d: %s", map_index,
2135 status.message());
2136 return;
2137 }
2138
2139 auto& bitmap = maps_bmp_[map_index];
2140
2141 // If bitmap doesn't exist yet (non-essential map), create it now
2142 if (!bitmap.is_active()) {
2143 overworld_.set_current_map(map_index);
2144 auto palette = overworld_.current_area_palette();
2145 try {
2146 bitmap.Create(kOverworldMapSize, kOverworldMapSize, 0x80,
2148 bitmap.SetPalette(palette);
2149 } catch (const std::bad_alloc& e) {
2150 LOG_ERROR("OverworldEditor", "Error allocating bitmap for map %d: %s",
2151 map_index, e.what());
2152 return;
2153 }
2154 }
2155
2156 if (!bitmap.texture() && bitmap.is_active()) {
2157 // Queue texture creation for this map
2160 }
2161}
2162
2164#ifdef __EMSCRIPTEN__
2165 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
2166 // stuttering. The tileset cache and debouncing provide enough optimization.
2167 return;
2168#endif
2169
2170 if (center_map < 0 || center_map >= zelda3::kNumOverworldMaps) {
2171 return;
2172 }
2173
2174 preload_queue_.clear();
2175
2176 // Calculate grid position (8x8 maps per world)
2177 int world_offset = (center_map / 64) * 64;
2178 int local_index = center_map % 64;
2179 int map_x = local_index % 8;
2180 int map_y = local_index / 8;
2181 int max_rows = (center_map >= zelda3::kSpecialWorldMapIdStart) ? 4 : 8;
2182
2183 // Add adjacent maps (4-connected neighbors)
2184 static const int dx[] = {-1, 1, 0, 0};
2185 static const int dy[] = {0, 0, -1, 1};
2186
2187 for (int i = 0; i < 4; ++i) {
2188 int nx = map_x + dx[i];
2189 int ny = map_y + dy[i];
2190
2191 // Check bounds (world grid; special world is only 4 rows high)
2192 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
2193 int neighbor_index = world_offset + ny * 8 + nx;
2194 // Only queue if not already built
2195 if (neighbor_index >= 0 && neighbor_index < zelda3::kNumOverworldMaps &&
2196 !overworld_.overworld_map(neighbor_index)->is_built()) {
2197 preload_queue_.push_back(neighbor_index);
2198 }
2199 }
2200 }
2201}
2202
2204#ifdef __EMSCRIPTEN__
2205 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
2206 // which causes unacceptable frame drops. Native builds use this for smoother UX.
2207 return;
2208#endif
2209
2210 if (preload_queue_.empty()) {
2211 return;
2212 }
2213
2214 // Process one map per frame to avoid blocking (native only)
2215 int map_to_preload = preload_queue_.back();
2216 preload_queue_.pop_back();
2217
2218 // Silent build - don't update UI state
2219 auto status = overworld_.EnsureMapBuilt(map_to_preload);
2220 if (!status.ok()) {
2221 // Log but don't interrupt - this is background work
2222 LOG_DEBUG("OverworldEditor", "Background preload of map %d failed: %s",
2223 map_to_preload, status.message().data());
2224 }
2225}
2226
2228 overworld_.mutable_overworld_map(map_index)->LoadAreaGraphics();
2229 status_ = overworld_.mutable_overworld_map(map_index)->BuildTileset();
2231 status_ = overworld_.mutable_overworld_map(map_index)->BuildTiles16Gfx(
2234 status_ = overworld_.mutable_overworld_map(map_index)->BuildBitmap(
2236 maps_bmp_[map_index].set_data(
2237 overworld_.mutable_overworld_map(map_index)->bitmap_data());
2238 maps_bmp_[map_index].set_modified(true);
2240}
2241
2243 // Use the new on-demand refresh system
2245}
2246
2255 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
2256 return;
2257 }
2258
2259 // Check if the map is actually visible or being edited
2260 bool is_current_map = (map_index == current_map_);
2261 bool is_current_world = (map_index / 0x40 == current_world_);
2262
2263 // For non-current maps in non-current worlds, defer the refresh
2264 if (!is_current_map && !is_current_world) {
2265 // Mark for deferred refresh - will be processed when the map becomes
2266 // visible
2267 maps_bmp_[map_index].set_modified(true);
2268 return;
2269 }
2270
2271 // For visible maps, do immediate refresh
2272 RefreshChildMapOnDemand(map_index);
2273}
2274
2279 auto* map = overworld_.mutable_overworld_map(map_index);
2280
2281 // Check what actually needs to be refreshed
2282 bool needs_graphics_rebuild = maps_bmp_[map_index].modified();
2283 bool needs_palette_rebuild = false; // Could be tracked more granularly
2284
2285 if (needs_graphics_rebuild) {
2286 // Only rebuild what's actually changed
2287 map->LoadAreaGraphics();
2288
2289 // Rebuild tileset only if graphics changed
2290 auto status = map->BuildTileset();
2291 if (!status.ok()) {
2292 LOG_ERROR("OverworldEditor", "Failed to build tileset for map %d: %s",
2293 map_index, status.message().data());
2294 return;
2295 }
2296
2297 // Rebuild tiles16 graphics
2298 status = map->BuildTiles16Gfx(*overworld_.mutable_tiles16(),
2299 overworld_.tiles16().size());
2300 if (!status.ok()) {
2301 LOG_ERROR("OverworldEditor",
2302 "Failed to build tiles16 graphics for map %d: %s", map_index,
2303 status.message().data());
2304 return;
2305 }
2306
2307 // Rebuild bitmap
2308 status = map->BuildBitmap(overworld_.GetMapTiles(current_world_));
2309 if (!status.ok()) {
2310 LOG_ERROR("OverworldEditor", "Failed to build bitmap for map %d: %s",
2311 map_index, status.message().data());
2312 return;
2313 }
2314
2315 // Update bitmap data
2316 maps_bmp_[map_index].set_data(map->bitmap_data());
2317 maps_bmp_[map_index].set_modified(false);
2318
2319 // Validate surface synchronization to help debug crashes
2320 if (!maps_bmp_[map_index].ValidateDataSurfaceSync()) {
2321 LOG_WARN("OverworldEditor",
2322 "Warning: Surface synchronization issue detected for map %d",
2323 map_index);
2324 }
2325
2326 // Queue texture update to ensure changes are visible
2327 if (maps_bmp_[map_index].texture()) {
2330 } else {
2333 }
2334 }
2335
2336 // Handle multi-area maps (large, wide, tall) with safe coordination
2337 // Use centralized version detection
2339 bool use_v3_area_sizes =
2341
2342 if (use_v3_area_sizes) {
2343 // Use v3 multi-area coordination
2344 RefreshMultiAreaMapsSafely(map_index, map);
2345 } else {
2346 // Legacy logic: only handle large maps for vanilla/v2
2347 if (map->is_large_map()) {
2348 RefreshMultiAreaMapsSafely(map_index, map);
2349 }
2350 }
2351}
2352
2368 zelda3::OverworldMap* map) {
2370
2371 auto area_size = map->area_size();
2372 if (area_size == AreaSizeEnum::SmallArea) {
2373 return; // No siblings to coordinate
2374 }
2375
2376 // Always work from parent perspective for consistent coordination
2377 int parent_id = map->parent();
2378
2379 // If we're not the parent, get the parent map to work from
2380 auto* parent_map = overworld_.mutable_overworld_map(parent_id);
2381 if (!parent_map) {
2382 LOG_WARN(
2383 "OverworldEditor",
2384 "RefreshMultiAreaMapsSafely: Could not get parent map %d for map %d",
2385 parent_id, map_index);
2386 return;
2387 }
2388
2389 LOG_DEBUG("OverworldEditor",
2390 "RefreshMultiAreaMapsSafely: Processing %s area from parent %d "
2391 "(trigger: %d)",
2392 (area_size == AreaSizeEnum::LargeArea) ? "large"
2393 : (area_size == AreaSizeEnum::WideArea) ? "wide"
2394 : "tall",
2395 parent_id, map_index);
2396
2397 // Determine all maps that are part of this multi-area structure
2398 // based on the parent's position and area size
2399 std::vector<int> sibling_maps;
2400
2401 switch (area_size) {
2402 case AreaSizeEnum::LargeArea:
2403 // Large Area: 2x2 grid (4 maps total)
2404 sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
2405 break;
2406
2407 case AreaSizeEnum::WideArea:
2408 // Wide Area: 2x1 grid (2 maps total, horizontally adjacent)
2409 sibling_maps = {parent_id, parent_id + 1};
2410 break;
2411
2412 case AreaSizeEnum::TallArea:
2413 // Tall Area: 1x2 grid (2 maps total, vertically adjacent)
2414 sibling_maps = {parent_id, parent_id + 8};
2415 break;
2416
2417 default:
2418 LOG_WARN("OverworldEditor",
2419 "RefreshMultiAreaMapsSafely: Unknown area size %d for map %d",
2420 static_cast<int>(area_size), map_index);
2421 return;
2422 }
2423
2424 // Refresh all siblings (including self if different from trigger)
2425 // The trigger map (map_index) was already processed by the caller,
2426 // so we skip it to avoid double-processing
2427 for (int sibling : sibling_maps) {
2428 // Skip the trigger map - it was already processed by RefreshChildMapOnDemand
2429 if (sibling == map_index) {
2430 continue;
2431 }
2432
2433 // Bounds check
2434 if (sibling < 0 || sibling >= zelda3::kNumOverworldMaps) {
2435 continue;
2436 }
2437
2438 // Check visibility - only immediately refresh visible maps
2439 bool is_current_map = (sibling == current_map_);
2440 bool is_current_world = (sibling / 0x40 == current_world_);
2441
2442 // Always mark sibling as needing refresh to ensure consistency
2443 maps_bmp_[sibling].set_modified(true);
2444
2445 if (is_current_map || is_current_world) {
2446 LOG_DEBUG("OverworldEditor",
2447 "RefreshMultiAreaMapsSafely: Refreshing sibling map %d",
2448 sibling);
2449
2450 // Direct refresh for visible siblings
2451 auto* sibling_map = overworld_.mutable_overworld_map(sibling);
2452 if (!sibling_map)
2453 continue;
2454
2455 sibling_map->LoadAreaGraphics();
2456
2457 auto status = sibling_map->BuildTileset();
2458 if (!status.ok()) {
2459 LOG_ERROR("OverworldEditor",
2460 "Failed to build tileset for sibling %d: %s", sibling,
2461 status.message().data());
2462 continue;
2463 }
2464
2465 status = sibling_map->BuildTiles16Gfx(*overworld_.mutable_tiles16(),
2466 overworld_.tiles16().size());
2467 if (!status.ok()) {
2468 LOG_ERROR("OverworldEditor",
2469 "Failed to build tiles16 for sibling %d: %s", sibling,
2470 status.message().data());
2471 continue;
2472 }
2473
2474 status = sibling_map->LoadPalette();
2475 if (!status.ok()) {
2476 LOG_ERROR("OverworldEditor",
2477 "Failed to load palette for sibling %d: %s", sibling,
2478 status.message().data());
2479 continue;
2480 }
2481
2482 status = sibling_map->BuildBitmap(overworld_.GetMapTiles(current_world_));
2483 if (!status.ok()) {
2484 LOG_ERROR("OverworldEditor",
2485 "Failed to build bitmap for sibling %d: %s", sibling,
2486 status.message().data());
2487 continue;
2488 }
2489
2490 // Update bitmap data
2491 maps_bmp_[sibling].set_data(sibling_map->bitmap_data());
2492
2493 // Set palette if bitmap has a valid surface
2494 if (maps_bmp_[sibling].is_active() && maps_bmp_[sibling].surface()) {
2495 maps_bmp_[sibling].SetPalette(sibling_map->current_palette());
2496 }
2497 maps_bmp_[sibling].set_modified(false);
2498
2499 // Queue texture update/creation
2500 if (maps_bmp_[sibling].texture()) {
2503 } else {
2504 EnsureMapTexture(sibling);
2505 }
2506 }
2507 // Non-visible siblings remain marked as modified for deferred refresh
2508 }
2509}
2510
2514 const auto current_map_palette = overworld_.current_area_palette();
2515 palette_ = current_map_palette;
2516 // Keep tile16 editor in sync with the currently active overworld palette
2517 tile16_editor_.set_palette(current_map_palette);
2518 // Ensure source graphics bitmap uses the refreshed palette so tile8 selector isn't blank.
2524 }
2525
2526 // Use centralized version detection
2528 bool use_v3_area_sizes =
2530
2531 if (use_v3_area_sizes) {
2532 // Use v3 area size system
2534 auto area_size = overworld_.overworld_map(current_map_)->area_size();
2535
2536 if (area_size != AreaSizeEnum::SmallArea) {
2537 // Get all sibling maps that need palette updates
2538 std::vector<int> sibling_maps;
2539 int parent_id = overworld_.overworld_map(current_map_)->parent();
2540
2541 switch (area_size) {
2542 case AreaSizeEnum::LargeArea:
2543 // 2x2 grid: parent, parent+1, parent+8, parent+9
2544 sibling_maps = {parent_id, parent_id + 1, parent_id + 8,
2545 parent_id + 9};
2546 break;
2547 case AreaSizeEnum::WideArea:
2548 // 2x1 grid: parent, parent+1
2549 sibling_maps = {parent_id, parent_id + 1};
2550 break;
2551 case AreaSizeEnum::TallArea:
2552 // 1x2 grid: parent, parent+8
2553 sibling_maps = {parent_id, parent_id + 8};
2554 break;
2555 default:
2556 break;
2557 }
2558
2559 // Update palette for all siblings - each uses its own loaded palette
2560 for (int sibling_index : sibling_maps) {
2561 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
2562 continue;
2563 }
2564 auto* sibling_map = overworld_.mutable_overworld_map(sibling_index);
2565 RETURN_IF_ERROR(sibling_map->LoadPalette());
2566 maps_bmp_[sibling_index].SetPalette(sibling_map->current_palette());
2567 }
2568 } else {
2569 // Small area - only update current map
2570 maps_bmp_[current_map_].SetPalette(current_map_palette);
2571 }
2572 } else {
2573 // Legacy logic for vanilla and v2 ROMs
2574 if (overworld_.overworld_map(current_map_)->is_large_map()) {
2575 // We need to update the map and its siblings if it's a large map
2576 for (int i = 1; i < 4; i++) {
2577 int sibling_index =
2578 overworld_.overworld_map(current_map_)->parent() + i;
2579 if (i >= 2)
2580 sibling_index += 6;
2581 auto* sibling_map = overworld_.mutable_overworld_map(sibling_index);
2582 RETURN_IF_ERROR(sibling_map->LoadPalette());
2583
2584 // SAFETY: Only set palette if bitmap has a valid surface
2585 // Use sibling map's own loaded palette
2586 if (maps_bmp_[sibling_index].is_active() &&
2587 maps_bmp_[sibling_index].surface()) {
2588 maps_bmp_[sibling_index].SetPalette(sibling_map->current_palette());
2589 }
2590 }
2591 }
2592
2593 // SAFETY: Only set palette if bitmap has a valid surface
2594 if (maps_bmp_[current_map_].is_active() &&
2595 maps_bmp_[current_map_].surface()) {
2596 maps_bmp_[current_map_].SetPalette(current_map_palette);
2597 }
2598 }
2599
2600 return absl::OkStatus();
2601}
2602
2604 // Mark the bitmap as modified to force refresh on next update
2605 if (map_index >= 0 && map_index < static_cast<int>(maps_bmp_.size())) {
2606 maps_bmp_[map_index].set_modified(true);
2607
2608 // Clear blockset cache
2609 current_blockset_ = 0xFF;
2610
2611 // Invalidate Overworld's tileset cache for this map and siblings
2612 // This ensures stale cached tilesets aren't reused after property changes
2614
2615 LOG_DEBUG("OverworldEditor",
2616 "ForceRefreshGraphics: Map %d marked for refresh", map_index);
2617 }
2618}
2619
2621 bool include_self) {
2622 if (map_index < 0 || map_index >= static_cast<int>(maps_bmp_.size())) {
2623 return;
2624 }
2625
2626 auto* map = overworld_.mutable_overworld_map(map_index);
2627 if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
2628 return; // No siblings for small areas
2629 }
2630
2631 int parent_id = map->parent();
2632 std::vector<int> siblings;
2633
2634 switch (map->area_size()) {
2636 siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
2637 break;
2639 siblings = {parent_id, parent_id + 1};
2640 break;
2642 siblings = {parent_id, parent_id + 8};
2643 break;
2644 default:
2645 return;
2646 }
2647
2648 for (int sibling : siblings) {
2649 if (sibling >= 0 && sibling < 0xA0) {
2650 // Skip self unless include_self is true
2651 if (sibling == map_index && !include_self) {
2652 continue;
2653 }
2654
2655 // Mark as modified FIRST before loading
2656 maps_bmp_[sibling].set_modified(true);
2657
2658 // Load graphics from ROM
2659 overworld_.mutable_overworld_map(sibling)->LoadAreaGraphics();
2660
2661 // CRITICAL FIX: Bypass visibility check - force immediate refresh
2662 // Call RefreshChildMapOnDemand() directly instead of
2663 // RefreshOverworldMapOnDemand()
2664 RefreshChildMapOnDemand(sibling);
2665
2666 LOG_DEBUG("OverworldEditor",
2667 "RefreshSiblingMapGraphics: Refreshed sibling map %d", sibling);
2668 }
2669 }
2670}
2671
2673 const auto& current_ow_map = *overworld_.mutable_overworld_map(current_map_);
2674
2675 // Use centralized version detection
2677 bool use_v3_area_sizes =
2679
2680 if (use_v3_area_sizes) {
2681 // Use v3 area size system
2683 auto area_size = current_ow_map.area_size();
2684
2685 if (area_size != AreaSizeEnum::SmallArea) {
2686 // Get all sibling maps that need property updates
2687 std::vector<int> sibling_maps;
2688 int parent_id = current_ow_map.parent();
2689
2690 switch (area_size) {
2691 case AreaSizeEnum::LargeArea:
2692 // 2x2 grid: parent+1, parent+8, parent+9 (skip parent itself)
2693 sibling_maps = {parent_id + 1, parent_id + 8, parent_id + 9};
2694 break;
2695 case AreaSizeEnum::WideArea:
2696 // 2x1 grid: parent+1 (skip parent itself)
2697 sibling_maps = {parent_id + 1};
2698 break;
2699 case AreaSizeEnum::TallArea:
2700 // 1x2 grid: parent+8 (skip parent itself)
2701 sibling_maps = {parent_id + 8};
2702 break;
2703 default:
2704 break;
2705 }
2706
2707 // Copy properties from parent map to all siblings
2708 for (int sibling_index : sibling_maps) {
2709 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
2710 continue;
2711 }
2712 auto& map = *overworld_.mutable_overworld_map(sibling_index);
2713 map.set_area_graphics(current_ow_map.area_graphics());
2714 map.set_area_palette(current_ow_map.area_palette());
2715 map.set_sprite_graphics(game_state_,
2716 current_ow_map.sprite_graphics(game_state_));
2717 map.set_sprite_palette(game_state_,
2718 current_ow_map.sprite_palette(game_state_));
2719 map.set_message_id(current_ow_map.message_id());
2720
2721 // CRITICAL FIX: Reload graphics after changing properties
2722 map.LoadAreaGraphics();
2723 }
2724 }
2725 } else {
2726 // Legacy logic for vanilla and v2 ROMs
2727 if (current_ow_map.is_large_map()) {
2728 // We need to copy the properties from the parent map to the children
2729 for (int i = 1; i < 4; i++) {
2730 int sibling_index = current_ow_map.parent() + i;
2731 if (i >= 2) {
2732 sibling_index += 6;
2733 }
2734 auto& map = *overworld_.mutable_overworld_map(sibling_index);
2735 map.set_area_graphics(current_ow_map.area_graphics());
2736 map.set_area_palette(current_ow_map.area_palette());
2737 map.set_sprite_graphics(game_state_,
2738 current_ow_map.sprite_graphics(game_state_));
2739 map.set_sprite_palette(game_state_,
2740 current_ow_map.sprite_palette(game_state_));
2741 map.set_message_id(current_ow_map.message_id());
2742
2743 // CRITICAL FIX: Reload graphics after changing properties
2744 map.LoadAreaGraphics();
2745 }
2746 }
2747 }
2748}
2749
2751 LOG_DEBUG("OverworldEditor", "RefreshTile16Blockset called");
2752 if (current_blockset_ ==
2753 overworld_.overworld_map(current_map_)->area_graphics()) {
2754 return absl::OkStatus();
2755 }
2757
2766 }
2767
2768 const auto tile16_data = overworld_.tile16_blockset_data();
2769
2772
2773 // Queue texture update for the atlas
2777 } else if (!tile16_blockset_.atlas.texture() &&
2779 // Create texture if it doesn't exist yet
2782 }
2783
2784 return absl::OkStatus();
2785}
2786
2788 // Skip if blockset not loaded or no pending changes
2789 if (!map_blockset_loaded_) {
2790 return;
2791 }
2792
2794 return;
2795 }
2796
2797 // Validate the atlas bitmap before modifying
2799 tile16_blockset_.atlas.vector().empty() ||
2800 tile16_blockset_.atlas.width() == 0 ||
2801 tile16_blockset_.atlas.height() == 0) {
2802 return;
2803 }
2804
2805 // Calculate tile positions in the atlas (8 tiles per row, each 16x16)
2806 constexpr int kTilesPerRow = 8;
2807 constexpr int kTileSize = 16;
2808 int atlas_width = tile16_blockset_.atlas.width();
2809 int atlas_height = tile16_blockset_.atlas.height();
2810
2811 bool atlas_modified = false;
2812
2813 // Iterate through all possible tile IDs to check for modifications
2814 // Note: This is a brute-force approach; a more efficient method would
2815 // maintain a list of modified tile IDs
2816 for (int tile_id = 0; tile_id < zelda3::kNumTile16Individual; ++tile_id) {
2817 if (!tile16_editor_.is_tile_modified(tile_id)) {
2818 continue;
2819 }
2820
2821 // Get the pending bitmap for this tile
2822 const gfx::Bitmap* pending_bmp =
2824 if (!pending_bmp || !pending_bmp->is_active() ||
2825 pending_bmp->vector().empty()) {
2826 continue;
2827 }
2828
2829 // Calculate position in the atlas
2830 int tile_x = (tile_id % kTilesPerRow) * kTileSize;
2831 int tile_y = (tile_id / kTilesPerRow) * kTileSize;
2832
2833 // Validate tile position is within atlas bounds
2834 if (tile_x + kTileSize > atlas_width || tile_y + kTileSize > atlas_height) {
2835 continue;
2836 }
2837
2838 // Copy pending bitmap data into the atlas at the correct position
2839 auto& atlas_data = tile16_blockset_.atlas.mutable_data();
2840 const auto& pending_data = pending_bmp->vector();
2841
2842 for (int y = 0; y < kTileSize && y < pending_bmp->height(); ++y) {
2843 for (int x = 0; x < kTileSize && x < pending_bmp->width(); ++x) {
2844 int atlas_idx = (tile_y + y) * atlas_width + (tile_x + x);
2845 int pending_idx = y * pending_bmp->width() + x;
2846
2847 if (atlas_idx >= 0 && atlas_idx < static_cast<int>(atlas_data.size()) &&
2848 pending_idx >= 0 &&
2849 pending_idx < static_cast<int>(pending_data.size())) {
2850 atlas_data[atlas_idx] = pending_data[pending_idx];
2851 atlas_modified = true;
2852 }
2853 }
2854 }
2855 }
2856
2857 // Only queue texture update if we actually modified something
2858 if (atlas_modified && tile16_blockset_.atlas.texture()) {
2862 }
2863}
2864
2866 // Handle middle-click for map interaction instead of right-click
2867 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle) &&
2868 ImGui::IsItemHovered()) {
2869 // Get the current map from mouse position (unscale coordinates)
2870 auto scaled_position = ow_map_canvas_.drawn_tile_position();
2871 float scale = ow_map_canvas_.global_scale();
2872 if (scale <= 0.0f)
2873 scale = 1.0f;
2874 int map_x = static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
2875 int map_y = static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
2876 int hovered_map = map_x + map_y * 8;
2877 if (current_world_ == 1) {
2878 hovered_map += 0x40;
2879 } else if (current_world_ == 2) {
2880 hovered_map += 0x80;
2881 }
2882
2883 // Only interact if we're hovering over a valid map
2884 if (hovered_map >= 0 && hovered_map < 0xA0) {
2885 // Toggle map lock or open properties panel
2886 if (current_map_lock_ && current_map_ == hovered_map) {
2887 current_map_lock_ = false;
2888 } else {
2889 current_map_lock_ = true;
2890 current_map_ = hovered_map;
2892 }
2893 }
2894 }
2895
2896 // Handle double-click to open properties panel (original behavior)
2897 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
2898 ImGui::IsItemHovered()) {
2900 }
2901}
2902
2903// Note: SetupOverworldCanvasContextMenu has been removed (Phase 3B).
2904// Context menu is now setup dynamically in DrawOverworldCanvas() via
2905// MapPropertiesSystem::SetupCanvasContextMenu() for context-aware menu items.
2906
2908 if (blockset_selector_) {
2909 blockset_selector_->ScrollToTile(current_tile16_);
2910 return;
2911 }
2912
2913 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
2914 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
2915 // window, and when called from CheckForSelectRectangle() during overworld
2916 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
2917 // the tile16 selector.
2918 //
2919 // The blockset_selector_ should always be available in modern code paths.
2920 // If it's not available, we skip scrolling rather than scroll the wrong
2921 // window.
2922 //
2923 // This fixes the bug where right-clicking to select tiles on the Dark World
2924 // causes the overworld canvas to scroll unexpectedly.
2925}
2926
2928 static bool init_properties = false;
2929
2930 if (!init_properties) {
2931 for (int i = 0; i < 0x40; i++) {
2932 std::string area_graphics_str = absl::StrFormat(
2933 "%02hX", overworld_.overworld_map(i)->area_graphics());
2935 ->push_back(area_graphics_str);
2936
2937 area_graphics_str = absl::StrFormat(
2938 "%02hX", overworld_.overworld_map(i + 0x40)->area_graphics());
2940 ->push_back(area_graphics_str);
2941
2942 std::string area_palette_str =
2943 absl::StrFormat("%02hX", overworld_.overworld_map(i)->area_palette());
2945 ->push_back(area_palette_str);
2946
2947 area_palette_str = absl::StrFormat(
2948 "%02hX", overworld_.overworld_map(i + 0x40)->area_palette());
2950 ->push_back(area_palette_str);
2951 std::string sprite_gfx_str = absl::StrFormat(
2952 "%02hX", overworld_.overworld_map(i)->sprite_graphics(1));
2954 ->push_back(sprite_gfx_str);
2955
2956 sprite_gfx_str = absl::StrFormat(
2957 "%02hX", overworld_.overworld_map(i)->sprite_graphics(2));
2959 ->push_back(sprite_gfx_str);
2960
2961 sprite_gfx_str = absl::StrFormat(
2962 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_graphics(1));
2964 ->push_back(sprite_gfx_str);
2965
2966 sprite_gfx_str = absl::StrFormat(
2967 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_graphics(2));
2969 ->push_back(sprite_gfx_str);
2970
2971 std::string sprite_palette_str = absl::StrFormat(
2972 "%02hX", overworld_.overworld_map(i)->sprite_palette(1));
2974 ->push_back(sprite_palette_str);
2975
2976 sprite_palette_str = absl::StrFormat(
2977 "%02hX", overworld_.overworld_map(i)->sprite_palette(2));
2979 ->push_back(sprite_palette_str);
2980
2981 sprite_palette_str = absl::StrFormat(
2982 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_palette(1));
2984 ->push_back(sprite_palette_str);
2985
2986 sprite_palette_str = absl::StrFormat(
2987 "%02hX", overworld_.overworld_map(i + 0x40)->sprite_palette(2));
2989 ->push_back(sprite_palette_str);
2990 }
2991 init_properties = true;
2992 }
2993
2994 ImGui::Text("Area Gfx LW/DW");
2995 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
2997 ImGui::SameLine();
2998 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3000 ImGui::Separator();
3001
3002 ImGui::Text("Sprite Gfx LW/DW");
3003 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3005 ImGui::SameLine();
3006 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3008 ImGui::SameLine();
3009 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3011 ImGui::SameLine();
3012 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3014 ImGui::Separator();
3015
3016 ImGui::Text("Area Pal LW/DW");
3017 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3019 ImGui::SameLine();
3020 properties_canvas_.UpdateInfoGrid(ImVec2(256, 256), 32,
3022
3023 static bool show_gfx_group = false;
3024 ImGui::Checkbox("Show Gfx Group Editor", &show_gfx_group);
3025 if (show_gfx_group) {
3026 gui::BeginWindowWithDisplaySettings("Gfx Group Editor", &show_gfx_group);
3029 }
3030}
3031
3033 // Unregister palette listener
3034 if (palette_listener_id_ >= 0) {
3037 }
3038
3040 current_graphics_set_.clear();
3041 all_gfx_loaded_ = false;
3042 map_blockset_loaded_ = false;
3043 return absl::OkStatus();
3044}
3045
3046absl::Status OverworldEditor::ApplyZSCustomOverworldASM(int target_version) {
3047 // Feature flag deprecated - ROM version gating is sufficient
3048 // User explicitly clicked upgrade button, so respect their request
3049
3050 // Validate target version
3051 if (target_version < 2 || target_version > 3) {
3052 return absl::InvalidArgumentError(absl::StrFormat(
3053 "Invalid target version: %d. Must be 2 or 3.", target_version));
3054 }
3055
3056 // Check current ROM version
3057 uint8_t current_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
3058 if (current_version != 0xFF && current_version >= target_version) {
3059 return absl::AlreadyExistsError(absl::StrFormat(
3060 "ROM is already version %d or higher", current_version));
3061 }
3062
3063 LOG_DEBUG("OverworldEditor", "Applying ZSCustomOverworld ASM v%d to ROM...",
3064 target_version);
3065
3066 // Initialize Asar wrapper
3067 auto asar_wrapper = std::make_unique<core::AsarWrapper>();
3068 RETURN_IF_ERROR(asar_wrapper->Initialize());
3069
3070 // Create backup of ROM data
3071 std::vector<uint8_t> original_rom_data = rom_->vector();
3072 std::vector<uint8_t> working_rom_data = original_rom_data;
3073
3074 try {
3075 // Determine which ASM file to apply and use GetResourcePath for proper
3076 // resolution
3077 std::string asm_file_name =
3078 (target_version == 3) ? "asm/yaze.asm" // Master file with v3
3079 : "asm/ZSCustomOverworld.asm"; // v2 standalone
3080
3081 // Use GetResourcePath to handle app bundles and various deployment
3082 // scenarios
3083 std::string asm_file_path = util::GetResourcePath(asm_file_name);
3084
3085 LOG_DEBUG("OverworldEditor", "Using ASM file: %s", asm_file_path.c_str());
3086
3087 // Verify file exists
3088 if (!std::filesystem::exists(asm_file_path)) {
3089 return absl::NotFoundError(
3090 absl::StrFormat("ASM file not found at: %s\n\n"
3091 "Expected location: assets/%s\n"
3092 "Make sure the assets directory is accessible.",
3093 asm_file_path, asm_file_name));
3094 }
3095
3096 // Apply the ASM patch
3097 auto patch_result =
3098 asar_wrapper->ApplyPatch(asm_file_path, working_rom_data);
3099 if (!patch_result.ok()) {
3100 return absl::InternalError(absl::StrFormat(
3101 "Failed to apply ASM patch: %s", patch_result.status().message()));
3102 }
3103
3104 const auto& result = patch_result.value();
3105 if (!result.success) {
3106 std::string error_details = "ASM patch failed with errors:\n";
3107 for (const auto& error : result.errors) {
3108 error_details += " - " + error + "\n";
3109 }
3110 if (!result.warnings.empty()) {
3111 error_details += "Warnings:\n";
3112 for (const auto& warning : result.warnings) {
3113 error_details += " - " + warning + "\n";
3114 }
3115 }
3116 return absl::InternalError(error_details);
3117 }
3118
3119 // Update ROM with patched data
3120 RETURN_IF_ERROR(rom_->LoadFromData(working_rom_data));
3121
3122 // Update version marker and feature flags
3124
3125 // Log symbols found during patching
3126 LOG_DEBUG("OverworldEditor",
3127 "ASM patch applied successfully. Found %zu symbols:",
3128 result.symbols.size());
3129 for (const auto& symbol : result.symbols) {
3130 LOG_DEBUG("OverworldEditor", " %s @ $%06X", symbol.name.c_str(),
3131 symbol.address);
3132 }
3133
3134 // Refresh overworld data to reflect changes
3136
3137 LOG_DEBUG("OverworldEditor",
3138 "ZSCustomOverworld v%d successfully applied to ROM",
3139 target_version);
3140 return absl::OkStatus();
3141
3142 } catch (const std::exception& e) {
3143 // Restore original ROM data on any exception
3144 auto restore_result = rom_->LoadFromData(original_rom_data);
3145 if (!restore_result.ok()) {
3146 LOG_ERROR("OverworldEditor", "Failed to restore ROM data: %s",
3147 restore_result.message().data());
3148 }
3149 return absl::InternalError(
3150 absl::StrFormat("Exception during ASM application: %s", e.what()));
3151 }
3152}
3153
3154absl::Status OverworldEditor::UpdateROMVersionMarkers(int target_version) {
3155 // Set the main version marker
3157 static_cast<uint8_t>(target_version);
3158
3159 // Enable feature flags based on target version
3160 if (target_version >= 2) {
3161 // v2+ features
3164
3165 LOG_DEBUG("OverworldEditor",
3166 "Enabled v2+ features: Custom BG colors, Main palettes");
3167 }
3168
3169 if (target_version >= 3) {
3170 // v3 features
3175
3176 LOG_DEBUG(
3177 "OverworldEditor",
3178 "Enabled v3+ features: Subscreen overlays, Animated GFX, Tile GFX "
3179 "groups, Mosaic");
3180
3181 // Initialize area size data for v3 (set all areas to small by default)
3182 for (int i = 0; i < 0xA0; i++) {
3183 (*rom_)[zelda3::kOverworldScreenSize + i] =
3184 static_cast<uint8_t>(zelda3::AreaSizeEnum::SmallArea);
3185 }
3186
3187 // Set appropriate sizes for known large areas
3188 const std::vector<int> large_areas = {
3189 0x00, 0x02, 0x05, 0x07, 0x0A, 0x0B, 0x0F, 0x10, 0x11, 0x12,
3190 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1D,
3191 0x1E, 0x25, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x30,
3192 0x32, 0x33, 0x34, 0x35, 0x37, 0x3A, 0x3B, 0x3C, 0x3F};
3193
3194 for (int area_id : large_areas) {
3195 if (area_id < 0xA0) {
3196 (*rom_)[zelda3::kOverworldScreenSize + area_id] =
3197 static_cast<uint8_t>(zelda3::AreaSizeEnum::LargeArea);
3198 }
3199 }
3200
3201 LOG_DEBUG("OverworldEditor", "Initialized area size data for %zu areas",
3202 large_areas.size());
3203 }
3204
3205 LOG_DEBUG("OverworldEditor", "ROM version markers updated to v%d",
3206 target_version);
3207 return absl::OkStatus();
3208}
3209
3211 if (!blockset_selector_) {
3212 return;
3213 }
3214
3216 blockset_selector_->SetSelectedTile(current_tile16_);
3217}
3218
3220 // Stub: toggle brush mode if available
3221}
3222
3224 // Stub: activate fill tool
3225}
3226
3228 current_tile16_ = std::max(0, current_tile16_ + delta);
3229}
3230
3231} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:130
const auto & vector() const
Definition rom.h:139
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:148
bool is_loaded() const
Definition rom.h:128
static Flags & get()
Definition features.h:92
EditorDependencies dependencies_
Definition editor.h:237
std::unique_ptr< UsageStatisticsCard > usage_stats_card_
absl::Status Clear() override
std::unique_ptr< MapPropertiesSystem > map_properties_system_
zelda3::OverworldItem current_item_
void HandleEntityInteraction()
Handle entity interaction in MOUSE mode Includes: right-click context menus, double-click navigation,...
static constexpr float kHoverBuildDelay
zelda3::OverworldEntranceTileTypes entrance_tiletypes_
zelda3::OverworldEntrance current_entrance_
void HandleTile16Edit()
Handle tile16 editing from context menu (MOUSE mode) Gets the tile16 under the cursor and opens the T...
absl::Status ApplyZSCustomOverworldASM(int target_version)
Apply ZSCustomOverworld ASM patch to upgrade ROM version.
std::optional< OverworldUndoPoint > current_paint_operation_
absl::Status CheckForCurrentMap()
Check for map changes and refresh if needed.
void CreateUndoPoint(int map_id, int world, int x, int y, int old_tile_id)
void ForceRefreshGraphics(int map_index)
std::vector< int > selected_tile16_ids_
void ProcessPendingEntityInsertion()
Process any pending entity insertion request Called from Update() - needed because ImGui::OpenPopup()...
Definition automation.cc:88
std::vector< OverworldUndoPoint > undo_stack_
void HandleEntityInsertion(const std::string &entity_type)
Handle entity insertion from context menu.
Definition automation.cc:75
zelda3::GameEntity * dragged_entity_
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > maps_bmp_
void CheckForOverworldEdits()
Check for tile edits - handles painting and selection.
absl::Status UpdateROMVersionMarkers(int target_version)
Update ROM version markers and feature flags after ASM patching.
void ProcessPreloadQueue()
Process one map from the preload queue (called each frame)
zelda3::OverworldExit current_exit_
void RefreshSiblingMapGraphics(int map_index, bool include_self=false)
std::vector< OverworldUndoPoint > redo_stack_
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.
std::unique_ptr< OverworldSidebar > sidebar_
static constexpr float kPreloadStartDelay
std::chrono::steady_clock::time_point last_paint_time_
void InvalidateGraphicsCache(int map_id=-1)
Invalidate cached graphics for a specific map or all maps.
void HandleEntityDoubleClick(zelda3::GameEntity *hovered_entity)
Handle double-click actions on entities (e.g., jump to room)
void HandleEntityContextMenus(zelda3::GameEntity *hovered_entity)
Handle right-click context menus for entities.
absl::Status SaveCurrentSelectionToScratch()
void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap *map)
Safely refresh multi-area maps without recursion.
void DrawOverworldCanvas()
Draw the main overworld canvas.
zelda3::Overworld & overworld()
Access the underlying Overworld data.
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_
std::unique_ptr< OverworldToolbar > toolbar_
void ProcessDeferredTextures()
Create textures for deferred map bitmaps on demand.
std::unique_ptr< gui::TileSelectorWidget > blockset_selector_
void DrawEntityEditorPopups()
Draw entity editor popups and update entity data.
absl::Status Paste() override
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset canvas to show the current selected tile16.
static constexpr auto kPaintBatchTimeout
static constexpr size_t kMaxUndoHistory
absl::Status LoadGraphics()
Load the Bitmap objects for each OverworldMap.
void RefreshChildMapOnDemand(int map_index)
On-demand child map refresh with selective updates.
std::unique_ptr< DebugWindowCard > debug_window_card_
void ApplyUndoPoint(const OverworldUndoPoint &point)
zelda3::GameEntity * current_entity_
void HandleKeyboardShortcuts()
Handle keyboard shortcuts for the Overworld Editor Shortcuts: 1-2 (modes), 3-8 (entities),...
void QueueAdjacentMapsForPreload(int center_map)
Queue adjacent maps for background pre-loading.
bool TogglePanel(size_t session_id, const std::string &base_card_id)
bool ShowPanel(size_t session_id, const std::string &base_card_id)
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)
const gfx::Bitmap * GetPendingTileBitmap(int tile_id) const
Get preview bitmap for a pending tile (nullptr if not modified)
bool has_pending_changes() const
Check if any tiles have uncommitted changes.
bool is_tile_modified(int tile_id) const
Check if a specific tile has pending changes.
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:35
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:371
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:115
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:102
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:378
static Arena & Get()
Definition arena.cc:20
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:581
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
TextureHandle texture() const
Definition bitmap.h:380
const std::vector< uint8_t > & vector() const
Definition bitmap.h:381
auto size() const
Definition bitmap.h:376
bool is_active() const
Definition bitmap.h:384
void set_modified(bool modified)
Definition bitmap.h:388
int height() const
Definition bitmap.h:374
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
int width() const
Definition bitmap.h:373
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:378
SDL_Surface * surface() const
Definition bitmap.h:379
RAII timer for automatic timing management.
void set_scrolling(ImVec2 scroll)
Definition canvas.h:446
auto selected_tile_pos() const
Definition canvas.h:492
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1075
auto global_scale() const
Definition canvas.h:494
auto select_rect_active() const
Definition canvas.h:490
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:237
auto selected_tiles() const
Definition canvas.h:491
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:1154
auto hover_mouse_pos() const
Definition canvas.h:558
void UpdateInfoGrid(ImVec2 bg_size, float grid_size=64.0f, int label_id=0)
Definition canvas.cc:539
auto drawn_tile_position() const
Definition canvas.h:450
bool DrawTilemapPainter(gfx::Tilemap &tilemap, int current_tile)
Definition canvas.cc:898
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
void ClearContextMenuItems()
Definition canvas.cc:776
auto mutable_labels(int i)
Definition canvas.h:539
void set_selected_tile_pos(ImVec2 pos)
Definition canvas.h:493
void DrawSelectRect(int current_map, int tile_size=0x10, float scale=1.0f)
Definition canvas.cc:1041
auto zero_point() const
Definition canvas.h:443
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1139
auto scrolling() const
Definition canvas.h:445
const ImVector< ImVec2 > & points() const
Definition canvas.h:439
auto selected_points() const
Definition canvas.h:556
Base class for all overworld and dungeon entities.
Definition common.h:31
virtual void UpdateMapProperties(uint16_t map_id, const void *context=nullptr)=0
Update entity properties based on map position.
enum yaze::zelda3::GameEntity::EntityType entity_type_
Represents an overworld exit that transitions from dungeon to overworld.
Represents a single Overworld map screen.
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
static bool SupportsAreaEnum(OverworldVersion version)
Check if ROM supports area enum system (v3+ only)
auto tile16_blockset_data() const
Definition overworld.h:520
auto current_area_palette() const
Definition overworld.h:512
void set_current_world(int world)
Definition overworld.h:535
int GetTileFromPosition(ImVec2 position) const
Definition overworld.h:449
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:36
absl::Status SaveMapProperties()
Save per-area graphics, palettes, and messages.
void ClearGraphicsConfigCache()
Clear entire graphics config cache Call when palette or graphics settings change globally.
Definition overworld.h:273
absl::Status SaveMap32Tiles()
Save tile32 definitions to ROM.
absl::Status SaveMap16Tiles()
Save tile16 definitions to ROM.
std::vector< gfx::Tile16 > tiles16() const
Definition overworld.h:487
void InvalidateSiblingMapCaches(int map_index)
Invalidate cached tilesets for a map and all its siblings.
auto is_loaded() const
Definition overworld.h:528
auto current_graphics() const
Definition overworld.h:498
absl::Status CreateTile32Tilemap()
Build tile32 tilemap from current tile16 data.
auto overworld_map(int i) const
Definition overworld.h:473
void set_current_map(int i)
Definition overworld.h:534
auto mutable_overworld_map(int i)
Definition overworld.h:479
absl::Status SaveEntrances()
Save entrance warp points to ROM.
absl::Status SaveExits()
Save exit return points to ROM.
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
Definition overworld.cc:888
absl::Status SaveItems()
Save hidden overworld items to ROM.
uint16_t GetTile(int x, int y) const
Definition overworld.h:536
absl::Status SaveOverworldMaps()
Save compressed map tile data to ROM.
auto mutable_sprites(int state)
Definition overworld.h:494
auto current_map_bitmap_data() const
Definition overworld.h:516
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:459
absl::Status SaveMusic()
Save per-area music IDs.
A class for managing sprites in the overworld and underworld.
Definition sprite.h:35
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_UPGRADE
Definition icons.h:2047
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_FORMAT_COLOR_FILL
Definition icons.h:830
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_LAYERS
Definition icons.h:1068
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size)
Editors are the view controllers for the application.
constexpr ImVec2 kOverworldCanvasSize(kOverworldMapSize *8, kOverworldMapSize *8)
constexpr unsigned int kOverworldMapSize
bool DrawSpriteEditorPopup(zelda3::Sprite &sprite)
Definition entity.cc:512
constexpr ImVec2 kCurrentGfxCanvasSize(0x100+1, 0x10 *0x40+1)
bool DrawOverworldEntrancePopup(zelda3::OverworldEntrance &entrance)
Definition entity.cc:110
bool DrawItemEditorPopup(zelda3::OverworldItem &item)
Definition entity.cc:382
constexpr int kTile16Size
void MoveEntityOnGrid(zelda3::GameEntity *entity, ImVec2 canvas_p0, ImVec2 scrolling, bool free_movement, float scale)
Move entity to grid-aligned position based on mouse.
Definition entity.cc:61
constexpr ImVec2 kGraphicsBinCanvasSize(0x100+1, kNumSheetsToLoad *0x40+1)
bool DrawExitEditorPopup(zelda3::OverworldExit &exit)
Definition entity.cc:195
void UpdateTilemap(IRenderer *renderer, Tilemap &tilemap, const std::vector< uint8_t > &data)
Definition tilemap.cc:34
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:270
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
constexpr const char * kOverworld
Definition popup_id.h:53
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1509
void BeginPadding(int i)
Definition style.cc:274
void BeginChildBothScrollbars(int id)
Definition style.cc:319
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1486
void EndNoPadding()
Definition style.cc:286
void CenterText(const char *text)
void EndPadding()
Definition style.cc:278
void BeginNoPadding()
Definition style.cc:282
void EndWindowWithDisplaySettings()
Definition style.cc:269
std::string MakePopupId(size_t session_id, const std::string &editor_name, const std::string &popup_name)
Generate session-aware popup IDs to prevent conflicts in multi-editor layouts.
Definition popup_id.h:23
void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size, ImGuiWindowFlags flags)
Definition style.cc:249
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:290
std::string GetResourcePath(const std::string &resource_path)
Definition file_util.cc:70
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
constexpr int OverworldCustomTileGFXGroupEnabled
constexpr int OverworldCustomAreaSpecificBGEnabled
constexpr int kOverworldScreenSize
Definition overworld.h:143
constexpr int kNumTile16Individual
Definition overworld.h:195
constexpr int kSpecialWorldMapIdStart
constexpr int OverworldCustomAnimatedGFXEnabled
constexpr int OverworldCustomMainPaletteEnabled
constexpr int kNumOverworldMaps
Definition common.h:85
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:89
constexpr int kDarkWorldMapIdStart
absl::StatusOr< OverworldEntranceTileTypes > LoadEntranceTileTypes(Rom *rom)
constexpr int OverworldCustomMosaicEnabled
constexpr int OverworldCustomSubscreenOverlayEnabled
#define IM_PI
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Overworld overworld
SharedClipboard * shared_clipboard
Definition editor.h:132
gfx::IRenderer * renderer
Definition editor.h:138
std::vector< std::pair< std::pair< int, int >, int > > tile_changes
static constexpr const char * kTile16Editor
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
std::optional< float > grid_step
Definition canvas.h:70