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