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#include "util/i18n/tr.h"
4
5#ifndef IM_PI
6#define IM_PI 3.14159265358979323846f
7#endif
8
9// C system headers
10#include <cmath>
11#include <cstddef>
12#include <cstdint>
13
14// C++ standard library headers
15#include <algorithm>
16#include <exception>
17#include <filesystem>
18#include <iostream>
19#include <memory>
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
46// Note: All overworld panels now self-register via REGISTER_PANEL macro:
47// AreaGraphicsPanel, DebugWindowPanel, GfxGroupsPanel, MapPropertiesPanel,
48// OverworldCanvasPanel, ScratchSpacePanel, Tile16EditorPanel, Tile16SelectorPanel,
49// Tile8SelectorPanel, UsageStatisticsPanel, V3SettingsPanel, OverworldItemListPanel
57#include "app/gfx/core/bitmap.h"
67#include "app/gui/core/icons.h"
69#include "app/gui/core/style.h"
74#include "core/asar_wrapper.h"
75#include "core/features.h"
76#include "core/project.h"
77#include "rom/rom.h"
78#include "util/file_util.h"
79#include "util/hex.h"
80#include "util/log.h"
81#include "util/macro.h"
82#include "zelda3/common.h"
90
91namespace yaze::editor {
92
93namespace {
94
96 const zelda3::OverworldItem& rhs) {
97 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
98 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.bg2_ == rhs.bg2_;
99}
100
102 const OverworldItemsSnapshot& rhs) {
103 if (lhs.items.size() != rhs.items.size()) {
104 return false;
105 }
106
107 for (size_t i = 0; i < lhs.items.size(); ++i) {
108 if (!ItemIdentityMatchesForUndo(lhs.items[i], rhs.items[i])) {
109 return false;
110 }
111 }
112
113 if (lhs.selected_item_identity.has_value() !=
114 rhs.selected_item_identity.has_value()) {
115 return false;
116 }
117 if (!lhs.selected_item_identity.has_value()) {
118 return true;
119 }
120
123}
124
125} // namespace
126
128 if (palette_listener_id_ >= 0) {
131 }
132
139
140 // These store callbacks and raw pointers into the editor. Release them while
141 // all editor-owned state is still alive rather than during member teardown.
142 sidebar_.reset();
144}
145
147 int& current_map) {
148 const int clamped_world = std::clamp(current_world, 0, 2);
149 int normalized_map = current_map;
150 if (normalized_map < 0 || normalized_map >= zelda3::kNumOverworldMaps) {
151 normalized_map =
152 std::min(clamped_world * 0x40, zelda3::kNumOverworldMaps - 1);
153 }
154
155 const int normalized_world = std::clamp(normalized_map / 0x40, 0, 2);
156 const bool changed =
157 normalized_world != current_world || normalized_map != current_map;
158 current_world = normalized_world;
159 current_map = normalized_map;
160 return changed;
161}
162
164 // Initialize renderer from dependencies
166
168 "Gfx Groups: selection syncs with the Graphics editor for this ROM "
169 "session "
170 "(this surface uses its own preview canvases).");
171
173
174 // Initialize MapRefreshCoordinator (must be before callbacks that use it)
176
177 if (rom_) {
178 upgrade_system_ = std::make_unique<zelda3::OverworldUpgradeSystem>(*rom_);
179 }
181 std::make_unique<EntityMutationService>(overworld_);
182
184
185 // Note: All WindowContent instances now self-register via REGISTER_PANEL macro
186 // and use ContentRegistry::Context to access the current editor.
187 // See comment at include section for the current set of view wrappers.
188
189 // Original initialization code below:
190 // Initialize MapPropertiesSystem with canvas and bitmap data
191 // Initialize cards
192 usage_stats_card_ = std::make_unique<UsageStatisticsCard>(&overworld_);
193 debug_window_card_ = std::make_unique<DebugWindowCard>();
194
195 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
197
198 // Set up refresh callbacks for MapPropertiesSystem
199 map_properties_system_->SetRefreshCallbacks(
200 [this]() { this->RefreshMapProperties(); },
201 [this]() { this->RefreshOverworldMap(); },
202 [this]() -> absl::Status { return this->RefreshMapPalette(); },
203 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
204 [this](int map_index) { this->ForceRefreshGraphics(map_index); });
205 map_properties_system_->SetMapSelectionCallback(
206 [this](int map_index, bool respect_pin) {
207 this->SelectMapForEditing(map_index, respect_pin);
208 });
209 map_properties_system_->SetPropertyEditCallback(
210 [this](const OverworldPropertyEdit& edit) {
211 return this->ApplyOverworldPropertyEdit(edit);
212 });
213 map_properties_system_->SetPropertyEditBatchCallback(
214 [this](const std::vector<OverworldPropertyEdit>& edits,
215 const std::string& description) {
216 return this->ApplyOverworldPropertyEdits(edits, description);
217 });
218 map_properties_system_->SetResourceLabelEditCallback(
219 [this](const std::string& type, int id, const std::string& label) {
220 return this->RenameProjectResourceLabelWithUndo(type, id, label);
221 });
222
223 // Initialize OverworldSidebar
224 sidebar_ = std::make_unique<OverworldSidebar>(&overworld_, rom_,
226
227 // Initialize OverworldEntityRenderer for entity visualization
228 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
230
231 // Initialize Toolbar
232 toolbar_ = std::make_unique<OverworldToolbar>();
233 toolbar_->on_world_changed = [this](int world) {
234 SwitchToWorld(world);
235 };
236 toolbar_->on_refresh_graphics = [this]() {
237 // Invalidate cached graphics for the current map area to force re-render
238 // with potentially new palette/graphics settings
241 };
242 toolbar_->on_refresh_map = [this]() {
244 };
245
246 toolbar_->on_save_to_scratch = [this]() {
248 };
249 toolbar_->on_load_from_scratch = [this]() {
251 };
252 toolbar_->on_upgrade_rom_version = [this](int) {
253 ImGui::OpenPopup("UpgradeROMVersion");
254 };
255 toolbar_->on_apply_property_edit = [this](const OverworldPropertyEdit& edit) {
256 return this->ApplyOverworldPropertyEdit(edit);
257 };
258 toolbar_->on_rename_resource_label = [this](const std::string& type, int id,
259 const std::string& label) {
260 return this->RenameProjectResourceLabelWithUndo(type, id, label);
261 };
262
263 // Initialize OverworldCanvasRenderer for canvas and panel drawing
264 canvas_renderer_ = std::make_unique<OverworldCanvasRenderer>(this);
265
269}
270
273 sink.on_set_editor_mode = [this](EditingMode mode) {
274 current_mode = mode;
277 } else {
279 }
280 };
281 sink.on_set_entity_mode = [this](EntityEditMode mode) {
282 entity_edit_mode_ = mode;
285 };
286 sink.on_toggle_brush = [this]() {
288 };
289 sink.on_activate_fill = [this]() {
291 };
292 sink.on_pick_tile_from_hover = [this]() {
294 };
295 sink.on_duplicate_selected = [this]() {
296 (void)DuplicateSelectedItem();
297 };
298 sink.on_nudge_selected = [this](int delta_x, int delta_y, bool shift_held) {
299 const int step = shift_held ? 16 : 1;
300 (void)this->NudgeSelectedItem(delta_x * step, delta_y * step);
301 };
302 sink.on_entity_context_menu = [this](zelda3::GameEntity* entity) {
304 auto* workbench = static_cast<OverworldEntityWorkbench*>(
306 "overworld.entity_workbench"));
307 if (workbench) {
308 workbench->OpenContextMenuFor(entity);
309 }
310 }
311 };
312 sink.on_entity_double_click = [this](zelda3::GameEntity* entity) {
313 if (!entity) {
314 return;
315 }
316 switch (entity->entity_type_) {
318 this->jump_to_tab_ =
319 static_cast<zelda3::OverworldExit*>(entity)->room_id_;
320 break;
322 this->jump_to_tab_ =
323 static_cast<zelda3::OverworldEntrance*>(entity)->entrance_id_;
324 break;
325 default:
326 break;
327 }
328 };
329 sink.on_toggle_item_list = [this]() {
331 return;
332 }
333 const size_t session_id =
337 };
338 sink.can_edit_items = [this]() {
340 };
341 sink.on_toggle_lock = [this]() {
343 };
344 sink.on_toggle_tile16_editor = [this]() {
346 return;
347 }
348 const size_t session_id =
352 };
353 sink.on_toggle_fullscreen = [this]() {
355 };
356 sink.on_undo = [this]() {
357 status_ = Undo();
358 };
359 sink.on_redo = [this]() {
360 status_ = Redo();
361 };
363 std::make_unique<OverworldInteractionCoordinator>(std::move(sink));
364}
365
369 deps.overworld = &overworld_;
370 deps.maps_bmp = &maps_bmp_;
378 deps.game_state = &game_state_;
379 deps.rom = rom_;
381
382 TilePaintingCallbacks callbacks;
383 callbacks.create_undo_point = [this](int map_id, int world, int x, int y,
384 int old_tile_id) {
385 this->CreateUndoPoint(map_id, world, x, y, old_tile_id);
386 };
387 callbacks.finalize_paint_operation = [this]() {
389 };
390 callbacks.refresh_overworld_map = [this]() {
391 this->RefreshOverworldMap();
392 };
393 callbacks.refresh_overworld_map_on_demand = [this](int map_index) {
394 this->RefreshOverworldMapOnDemand(map_index);
395 };
396 callbacks.scroll_blockset_to_current_tile = [this]() {
398 };
399 callbacks.request_tile16_selection = [this](int tile_id) {
400 this->RequestTile16Selection(tile_id);
401 };
402
403 tile_painting_ = std::make_unique<TilePaintingManager>(deps, callbacks);
404}
405
409 ctx.overworld = &overworld_;
410 ctx.rom = rom_;
420 ctx.maps_bmp = &maps_bmp_;
423
425 callbacks.refresh_overworld_map = [this]() {
426 this->RefreshOverworldMap();
427 };
428 callbacks.refresh_tile16_blockset = [this]() -> absl::Status {
429 return this->RefreshTile16Blockset();
430 };
431 callbacks.ensure_map_texture = [this](int map_index) {
432 this->EnsureMapTexture(map_index);
433 };
434 callbacks.select_map_for_editing = [this](int map_index, bool respect_pin) {
435 this->SelectMapForEditing(map_index, respect_pin);
436 };
437 callbacks.pick_tile16_from_hovered_canvas = [this]() -> bool {
438 return this->PickTile16FromHoveredCanvas();
439 };
440 callbacks.is_entity_hovered = [this]() -> bool {
441 return entity_renderer_ && entity_renderer_->hovered_entity() != nullptr;
442 };
443
444 canvas_nav_ = std::make_unique<CanvasNavigationManager>();
445 canvas_nav_->Initialize(ctx, callbacks);
446}
447
449 current_entity_ = entity;
450 if (!entity) {
452 return;
453 }
455 selected_item_identity_ = *static_cast<zelda3::OverworldItem*>(entity);
456 } else {
458 }
459}
460
466
467absl::Status OverworldEditor::Load() {
468 gfx::ScopedTimer timer("OverworldEditor::Load");
469
470 LOG_DEBUG("OverworldEditor", "Loading overworld.");
471 if (!rom_ || !rom_->is_loaded()) {
472 return absl::FailedPreconditionError("ROM not loaded");
473 }
474
476 dependencies_.window_manager->RegisterPanelAlias("overworld.debug_window",
477 "overworld.debug");
478 }
479
480 // Clear undo/redo state when loading new ROM data
483
488
489 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
493 [this](int id) { current_tile16_ = id; });
494
495 // Set up callback for when tile16 changes are committed
497 [this](const std::vector<Tile16Commit>& commits) -> absl::Status {
498 auto* tiles16 = overworld_.mutable_tiles16();
499 for (const auto& commit : commits) {
500 if (commit.tile_id >= 0 &&
501 commit.tile_id < static_cast<int>(tiles16->size())) {
502 (*tiles16)[commit.tile_id] = commit.tile_data;
503 }
504 }
505
506 // Regenerate the overworld editor's tile16 blockset
508
509 // Force refresh of the current overworld map to show changes
511
512 LOG_DEBUG("OverworldEditor",
513 "Overworld editor refreshed after Tile16 changes");
514 return absl::OkStatus();
515 });
516
517 // Set up entity insertion callback for MapPropertiesSystem
519 map_properties_system_->SetEntityCallbacks(
520 [this](const std::string& entity_type) {
521 HandleEntityInsertion(entity_type);
522 });
523
524 // Set up tile16 edit callback for context menu in MOUSE mode
525 map_properties_system_->SetTile16SampleCallback(
526 [this]() { return PickTile16FromHoveredCanvas(); });
527 map_properties_system_->SetTile16EditCallback(
528 [this]() { HandleTile16Edit(); });
529 }
530
532
533 // Register as palette listener to refresh graphics when palettes change
534 if (palette_listener_id_ < 0) {
536 [this](const std::string& group_name, int palette_index) {
537 // Only respond to overworld-related palette changes
538 if (group_name == "ow_main" || group_name == "ow_animated" ||
539 group_name == "ow_aux" || group_name == "grass") {
540 LOG_DEBUG("OverworldEditor",
541 "Palette change detected: %s, refreshing current map",
542 group_name.c_str());
543 // Refresh current map graphics to reflect palette changes
544 if (current_map_ >= 0 && all_gfx_loaded_) {
546 }
547 }
548 });
549 LOG_DEBUG("OverworldEditor", "Registered as palette listener (ID: %d)",
551 }
552
553 all_gfx_loaded_ = true;
554
555 auto scratch_status = LoadScratchPad();
556 if (!scratch_status.ok()) {
557 LOG_WARN("OverworldEditor", "Failed to load scratch pad: %s",
558 std::string(scratch_status.message()).c_str());
559 }
560
562 const int pending = *pending_tile16_selection_after_gfx_;
564 RequestTile16Selection(pending);
565 }
566
567 return absl::OkStatus();
568}
569
571 status_ = absl::OkStatus();
572
573 // Safety check: Ensure ROM is loaded and graphics are ready
574 if (!rom_ || !rom_->is_loaded()) {
576 return absl::OkStatus();
577 }
578
579 if (!all_gfx_loaded_) {
580 gui::DrawEmptyState(gui::EmptyLoading("Loading overworld graphics…"));
581 return absl::OkStatus();
582 }
583
585
586 // Process deferred textures for smooth loading
588
589 // Update blockset atlas with any pending tile16 changes for live preview
590 // Tile cache now uses copy semantics so this is safe to enable
593 }
594
595 // Early return if window_manager is not available
596 // (panels won't be drawn without it, so no point continuing)
598 return status_;
599 }
600
602 return status_;
603 }
604
605 // ===========================================================================
606 // Main Overworld Canvas
607 // ===========================================================================
608 // The panels (Tile16 Selector, Area Graphics, etc.) are now managed by
609 // WindowContent/WorkspaceWindowManager and drawn automatically. This section only
610 // handles the main canvas and toolbar.
611
612 // ===========================================================================
613 // Non-Panel Windows (not managed by WindowContent system)
614 // ===========================================================================
615 // These are separate feature windows, not part of the panel system
616
617 // Custom Background Color Editor
619 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
620 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color",
622 if (rom_->is_loaded() && overworld_.is_loaded() &&
624 map_properties_system_->DrawCustomBackgroundColorEditor(
626 }
627 }
628 ImGui::End();
629 }
630
631 // Visual Effects Editor (Subscreen Overlays)
633 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
634 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor",
636 if (rom_->is_loaded() && overworld_.is_loaded() &&
638 map_properties_system_->DrawOverlayEditor(current_map_,
640 }
641 }
642 ImGui::End();
643 }
644
645 // Note: Tile16 Editor is now managed as an WindowContent (Tile16EditorPanel)
646 // It uses UpdateAsPanel() which provides a context menu instead of MenuBar
647
648 if (auto* workbench = GetWorkbench()) {
649 workbench->ProcessPendingInsertion(entity_mutation_service_.get(),
651 workbench->DrawPopups();
652 }
653
654 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr,
655 ImGuiWindowFlags_AlwaysAutoResize)) {
656 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
657 ImGui::Separator();
658 ImGui::TextWrapped(
659 tr("This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
660 "enabling advanced overworld features."));
661 ImGui::Separator();
662
663 const uint8_t current_version =
665 : 0xFF;
666 ImGui::Text(tr("Current Version: %s"),
667 current_version == 0xFF
668 ? "Vanilla"
669 : absl::StrFormat("v%d", current_version).c_str());
670
671 static int target_version = 3;
672 ImGui::RadioButton(tr("v2 (Basic features)"), &target_version, 2);
673 ImGui::SameLine();
674 ImGui::RadioButton(tr("v3 (All features)"), &target_version, 3);
675
676 ImGui::Separator();
677
678 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150.0f, 0.0f))) {
679 auto upgrade_status =
680 upgrade_system_ != nullptr
681 ? upgrade_system_->ApplyZSCustomOverworldASM(target_version)
682 : absl::FailedPreconditionError("Upgrade system not initialized");
683 if (upgrade_status.ok()) {
684 status_ = Clear();
685 if (status_.ok()) {
686 status_ = Load();
687 }
688 if (status_.ok()) {
689 ImGui::CloseCurrentPopup();
690 }
691 } else {
692 LOG_ERROR("OverworldEditor", "Upgrade failed: %s",
693 upgrade_status.message().data());
694 }
695 }
696 ImGui::SameLine();
697 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150.0f, 0.0f))) {
698 ImGui::CloseCurrentPopup();
699 }
700
701 ImGui::EndPopup();
702 }
703
704 // ===========================================================================
705 // Centralized Entity Interaction Logic (extracted to dedicated method)
706 // ===========================================================================
707 zelda3::GameEntity* hovered_entity =
708 entity_renderer_ ? entity_renderer_->hovered_entity() : nullptr;
710 interaction_coordinator_->Update(hovered_entity);
711 }
712
713 return absl::OkStatus();
714}
715
718
719 if (canvas_renderer_) {
720 canvas_renderer_->DrawOverworldCanvas();
721 }
722}
723
726
729 if (const auto* map = overworld_.overworld_map(current_map_)) {
730 current_parent_ = map->parent();
731 } else {
733 }
734
735 if (changed) {
736 LOG_WARN("OverworldEditor",
737 "Normalized stale overworld selection to world=%d map=%d",
739 }
740 return changed;
741}
742
743void OverworldEditor::SelectMapForEditing(int map_id, bool respect_pin) {
744 if (respect_pin && current_map_lock_) {
745 return;
746 }
747 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
748 return;
749 }
750 const auto* map = overworld_.overworld_map(map_id);
751 if (!map) {
752 return;
753 }
754
756 current_map_ = map_id;
757 current_world_ = std::clamp(map_id / 0x40, 0, 2);
758 current_parent_ = map->parent();
761
763 if (!status_.ok()) {
765 return;
766 }
767
769 if (all_gfx_loaded_) {
772 }
773}
774
775void OverworldEditor::PrimeWorldMaps(int world, bool process_texture_queue) {
776 if (map_texture_)
777 map_texture_->PrimeWorldMaps(world, process_texture_queue);
778}
779
782
783 const int clamped_world = std::clamp(world, 0, 2);
784 const int local_map = current_map_ & 0x3F;
785 const int world_start = clamped_world * 0x40;
786 const int maps_remaining = zelda3::kNumOverworldMaps - world_start;
787 if (maps_remaining <= 0) {
788 return;
789 }
790
791 hovered_map_ = -1;
792 SelectMapForEditing(world_start + std::min(local_map, maps_remaining - 1),
793 false);
795
797 if (!status_.ok()) {
799 return;
800 }
801
807}
808
814
816 const zelda3::OverworldItem& item_identity) {
817 auto* item = FindItemByIdentity(&overworld_, item_identity);
818 if (!item) {
819 return false;
820 }
821
823 current_entity_ = item;
825 auto* workbench = static_cast<OverworldEntityWorkbench*>(
827 "overworld.entity_workbench"));
828 if (workbench) {
829 workbench->SetActiveEntity(item);
830 }
831 }
832 return true;
833}
834
837 current_entity_ = nullptr;
839 auto* workbench = static_cast<OverworldEntityWorkbench*>(
841 "overworld.entity_workbench"));
842 if (workbench) {
843 workbench->SetActiveEntity(nullptr);
844 }
845 }
846}
847
850 return nullptr;
851 return static_cast<OverworldEntityWorkbench*>(
853 "overworld.entity_workbench"));
854}
855
857 if (!selected_item_identity_.has_value()) {
858 return nullptr;
859 }
860
862 if (!item) {
864 return nullptr;
865 }
866
867 current_entity_ = item;
868 if (auto* workbench = GetWorkbench()) {
869 workbench->SetActiveEntity(item);
870 }
871 return item;
872}
873
875 return const_cast<OverworldEditor*>(this)->GetSelectedItem();
876}
877
884
886 const OverworldItemsSnapshot& snapshot) {
887 auto* items = overworld_.mutable_all_items();
888 if (!items) {
889 return;
890 }
891
892 *items = snapshot.items;
894 if (selected_item_identity_.has_value()) {
895 auto* selected_item =
897 if (selected_item) {
898 current_entity_ = selected_item;
899 if (auto* workbench = GetWorkbench()) {
900 workbench->SetActiveEntity(selected_item);
901 }
902 } else {
904 }
905 } else {
907 }
908
909 if (rom_) {
910 rom_->set_dirty(true);
911 }
913}
914
916 std::string description) {
918 if (ItemSnapshotsEqual(before, after)) {
919 return;
920 }
921
922 undo_manager_.Push(std::make_unique<OverworldItemsEditAction>(
923 std::move(before), std::move(after),
924 [this](const OverworldItemsSnapshot& snapshot) {
925 RestoreItemUndoSnapshot(snapshot);
926 },
927 std::move(description)));
928}
929
930bool OverworldEditor::DuplicateSelectedItem(int offset_x, int offset_y) {
931 auto* selected_item = GetSelectedItem();
932 if (!selected_item) {
935 "Select an overworld item first (Item Mode: key 5)",
937 }
938 return false;
939 }
940
941 auto before_snapshot = CaptureItemUndoSnapshot();
944 duplicate_result = entity_mutation_service_->DuplicateItem(
945 *selected_item, offset_x, offset_y);
946 } else {
947 auto duplicate_or = DuplicateItemByIdentity(&overworld_, *selected_item,
948 offset_x, offset_y);
949 if (duplicate_or.ok()) {
950 duplicate_result.entity = *duplicate_or;
951 duplicate_result.status = absl::OkStatus();
952 } else {
953 duplicate_result.status = duplicate_or.status();
954 duplicate_result.error_message =
955 "Failed to duplicate overworld item: " +
956 std::string(duplicate_or.status().message());
957 }
958 }
959 if (!duplicate_result.ok()) {
962 duplicate_result.error_message.empty()
963 ? "Failed to duplicate overworld item"
964 : duplicate_result.error_message,
966 }
967 return false;
968 }
969
970 auto* duplicated_item =
971 static_cast<zelda3::OverworldItem*>(duplicate_result.entity);
972 selected_item_identity_ = *duplicated_item;
973 current_entity_ = duplicated_item;
974 if (auto* workbench = GetWorkbench()) {
975 workbench->SetActiveEntity(duplicated_item);
976 }
977 PushItemUndoAction(std::move(before_snapshot),
978 absl::StrFormat("Duplicate overworld item 0x%02X",
979 static_cast<int>(duplicated_item->id_)));
980 rom_->set_dirty(true);
983 absl::StrFormat("Duplicated item 0x%02X",
984 static_cast<int>(duplicated_item->id_)),
986 }
987 return true;
988}
989
990bool OverworldEditor::NudgeSelectedItem(int delta_x, int delta_y) {
991 auto* selected_item = GetSelectedItem();
992 if (!selected_item) {
993 return false;
994 }
995
996 auto before_snapshot = CaptureItemUndoSnapshot();
997 auto status = NudgeItem(selected_item, delta_x, delta_y);
998 if (!status.ok()) {
1000 dependencies_.toast_manager->Show("Failed to move selected item",
1002 }
1003 return false;
1004 }
1005
1006 selected_item_identity_ = *selected_item;
1007 current_entity_ = selected_item;
1008 if (auto* workbench = GetWorkbench()) {
1009 workbench->SetActiveEntity(selected_item);
1010 }
1012 std::move(before_snapshot),
1013 absl::StrFormat("Move overworld item 0x%02X (%+d,%+d)",
1014 static_cast<int>(selected_item->id_), delta_x, delta_y));
1015 rom_->set_dirty(true);
1016 return true;
1017}
1018
1020 auto* selected_item = GetSelectedItem();
1021 if (!selected_item) {
1022 return false;
1023 }
1024
1025 auto before_snapshot = CaptureItemUndoSnapshot();
1026 const zelda3::OverworldItem selected_identity = *selected_item;
1027 const uint8_t deleted_item_id = selected_identity.id_;
1028 absl::Status remove_status =
1029 absl::FailedPreconditionError("Entity mutation service not initialized");
1031 remove_status =
1032 entity_mutation_service_->DeleteItem(selected_identity).status;
1033 } else {
1034 remove_status = RemoveItemByIdentity(&overworld_, selected_identity);
1035 }
1036 if (!remove_status.ok()) {
1038 dependencies_.toast_manager->Show("Failed to delete selected item",
1040 }
1041 return false;
1042 }
1043
1044 auto* nearest_item =
1046 ? entity_mutation_service_->ResolveNextSelection(selected_identity)
1047 : FindNearestItemForSelection(&overworld_, selected_identity);
1048 if (nearest_item) {
1049 selected_item_identity_ = *nearest_item;
1050 current_entity_ = nearest_item;
1051 if (auto* workbench = GetWorkbench()) {
1052 workbench->SetActiveEntity(nearest_item);
1053 }
1054 } else {
1056 }
1057
1058 PushItemUndoAction(std::move(before_snapshot),
1059 absl::StrFormat("Delete overworld item 0x%02X",
1060 static_cast<int>(deleted_item_id)));
1061 rom_->set_dirty(true);
1063 if (nearest_item) {
1065 absl::StrFormat("Deleted item 0x%02X (selected nearest 0x%02X)",
1066 static_cast<int>(deleted_item_id),
1067 static_cast<int>(nearest_item->id_)),
1069 } else {
1071 absl::StrFormat("Deleted overworld item 0x%02X",
1072 static_cast<int>(deleted_item_id)),
1074 }
1075 }
1076 return true;
1077}
1078
1080 tile_painting_->CheckForOverworldEdits();
1081}
1082
1085 return absl::FailedPreconditionError("Clipboard unavailable");
1086 }
1087 // If a rectangle selection exists, copy its tile16 IDs into shared clipboard
1089 !ow_map_canvas_.selected_points().empty()) {
1090 std::vector<int> ids;
1091 // selected_points are now stored in world coordinates
1092 const auto start = ow_map_canvas_.selected_points()[0];
1093 const auto end = ow_map_canvas_.selected_points()[1];
1094 const int start_x =
1095 static_cast<int>(std::floor(std::min(start.x, end.x) / 16.0f));
1096 const int end_x =
1097 static_cast<int>(std::floor(std::max(start.x, end.x) / 16.0f));
1098 const int start_y =
1099 static_cast<int>(std::floor(std::min(start.y, end.y) / 16.0f));
1100 const int end_y =
1101 static_cast<int>(std::floor(std::max(start.y, end.y) / 16.0f));
1102 const int width = end_x - start_x + 1;
1103 const int height = end_y - start_y + 1;
1104 ids.reserve(width * height);
1107 for (int y = start_y; y <= end_y; ++y) {
1108 for (int x = start_x; x <= end_x; ++x) {
1109 ids.push_back(overworld_.GetTile(x, y));
1110 }
1111 }
1112
1117 return absl::OkStatus();
1118 }
1119 // Single tile copy fallback
1120 if (current_tile16_ >= 0) {
1125 return absl::OkStatus();
1126 }
1127 return absl::FailedPreconditionError("Nothing selected to copy");
1128}
1129
1132 return absl::FailedPreconditionError("Clipboard unavailable");
1133 }
1135 return absl::FailedPreconditionError("Clipboard empty");
1136 }
1137 if (ow_map_canvas_.points().empty() &&
1139 return absl::FailedPreconditionError("No paste target");
1140 }
1141
1142 // Determine paste anchor position (use current mouse drawn tile position)
1143 // Unscale coordinates to get world position
1144 const ImVec2 scaled_anchor = ow_map_canvas_.drawn_tile_position();
1145 float scale = ow_map_canvas_.global_scale();
1146 if (scale <= 0.0f)
1147 scale = 1.0f;
1148 const ImVec2 anchor =
1149 ImVec2(scaled_anchor.x / scale, scaled_anchor.y / scale);
1150
1151 // Compute anchor in tile16 grid within the current map
1152 const int tile16_x =
1153 (static_cast<int>(anchor.x) % kOverworldMapSize) / kTile16Size;
1154 const int tile16_y =
1155 (static_cast<int>(anchor.y) % kOverworldMapSize) / kTile16Size;
1156
1157 auto& selected_world =
1158 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
1159 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
1160 : overworld_.mutable_map_tiles()->special_world;
1161
1162 const int world_offset = current_world_ * 0x40;
1163 const int local_map = current_map_ - world_offset;
1164 const int superY = local_map / 8;
1165 const int superX = local_map % 8;
1166
1170
1171 // Guard
1172 if (width * height != static_cast<int>(ids.size())) {
1173 return absl::InternalError("Clipboard dimensions mismatch");
1174 }
1175
1176 bool any_changed = false;
1177 for (int dy = 0; dy < height; ++dy) {
1178 for (int dx = 0; dx < width; ++dx) {
1179 const int id = ids[dy * width + dx];
1180 const int gx = tile16_x + dx;
1181 const int gy = tile16_y + dy;
1182
1183 const int global_x = superX * 32 + gx;
1184 const int global_y = superY * 32 + gy;
1185 if (global_x < 0 || global_x >= 256 || global_y < 0 || global_y >= 256)
1186 continue;
1187 const int old_tile_id = selected_world[global_x][global_y];
1188 if (old_tile_id == id) {
1189 continue;
1190 }
1191 CreateUndoPoint(current_map_, current_world_, global_x, global_y,
1192 old_tile_id);
1193 selected_world[global_x][global_y] = id;
1194 any_changed = true;
1195 }
1196 }
1197
1198 if (any_changed) {
1200 rom_->set_dirty(true);
1202 }
1203 return absl::OkStatus();
1204}
1205
1207 if (canvas_nav_)
1208 return canvas_nav_->CheckForCurrentMap();
1209 return absl::OkStatus();
1210}
1211
1213 // Delegate to the existing GfxGroupEditor
1214 if (rom_ && rom_->is_loaded()) {
1215 return gfx_group_editor_.Update();
1216 } else {
1217 gui::DrawEmptyState(gui::EmptyNoRom(/*compact=*/true));
1218 return absl::OkStatus();
1219 }
1220}
1221
1222// DrawV3Settings - now in OverworldCanvasRenderer
1223
1224// DrawMapProperties - now in OverworldCanvasRenderer
1225
1227 // HACK MANIFEST VALIDATION
1228 const bool saving_maps =
1230 if (saving_maps && dependencies_.project &&
1232 const auto& manifest = dependencies_.project->hack_manifest;
1233 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1234
1235 // Calculate memory ranges that would be written by overworld map saves.
1236 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1237 // address space (LoROM), so convert before analysis.
1238 auto ranges = overworld_.GetProjectedWriteRanges();
1240 manifest, write_policy, ranges, "overworld maps", "OverworldEditor",
1242 }
1243
1244 if (saving_maps) {
1245 const auto profile = zelda3::DetectOverworldRomProfile(*rom_);
1247 if (profile.has_expanded_tile32) {
1249 } else {
1251 }
1252 if (profile.has_expanded_tile16) {
1254 } else {
1256 }
1258 }
1259 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1261 }
1262 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1264 }
1265 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1267 }
1268 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1272 }
1273 return absl::OkStatus();
1274}
1275
1276// ============================================================================
1277// Undo/Redo System Implementation
1278// ============================================================================
1279
1281 switch (world) {
1282 case 0:
1283 return overworld_.mutable_map_tiles()->light_world;
1284 case 1:
1285 return overworld_.mutable_map_tiles()->dark_world;
1286 default:
1287 return overworld_.mutable_map_tiles()->special_world;
1288 }
1289}
1290
1291void OverworldEditor::CreateUndoPoint(int map_id, int world, int x, int y,
1292 int old_tile_id) {
1293 auto now = std::chrono::steady_clock::now();
1294
1295 // Check if we should batch with current operation (same map, same world,
1296 // within timeout)
1297 if (current_paint_operation_.has_value() &&
1298 current_paint_operation_->map_id == map_id &&
1299 current_paint_operation_->world == world &&
1301 // Add to existing operation
1302 current_paint_operation_->tile_changes.emplace_back(std::make_pair(x, y),
1303 old_tile_id);
1304 } else {
1305 // Finalize any pending operation before starting a new one
1307
1308 // Start new operation
1310 OverworldUndoPoint{.map_id = map_id,
1311 .world = world,
1312 .tile_changes = {{{x, y}, old_tile_id}},
1313 .timestamp = now};
1314 }
1315
1316 last_paint_time_ = now;
1317}
1318
1320 if (!current_paint_operation_.has_value()) {
1321 return;
1322 }
1323
1324 // Push to the UndoManager (new framework path).
1325 auto& world_tiles = GetWorldTiles(current_paint_operation_->world);
1326 std::vector<OverworldTileChange> changes;
1327 changes.reserve(current_paint_operation_->tile_changes.size());
1328 for (const auto& [coords, old_tile_id] :
1329 current_paint_operation_->tile_changes) {
1330 auto [x, y] = coords;
1331 int new_tile_id = world_tiles[x][y];
1332 changes.push_back({x, y, old_tile_id, new_tile_id});
1333 }
1334 auto action = std::make_unique<OverworldTilePaintAction>(
1336 std::move(changes), &overworld_, [this]() { RefreshOverworldMap(); });
1337 undo_manager_.Push(std::move(action));
1338
1340}
1341
1343 const OverworldPropertyEdit& edit, bool record_undo) {
1345 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1346 }
1347
1348 if (!record_undo) {
1349 return map_properties_system_->ApplyPropertyEditDirect(edit);
1350 }
1351
1353 ASSIGN_OR_RETURN(const int before_value,
1354 map_properties_system_->ReadPropertyValue(edit));
1355
1356 OverworldPropertyEdit before = edit;
1357 before.value = before_value;
1358 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1359
1360 ASSIGN_OR_RETURN(const int after_value,
1361 map_properties_system_->ReadPropertyValue(edit));
1362 if (after_value == before_value) {
1363 return absl::OkStatus();
1364 }
1365
1366 OverworldPropertyEdit after = edit;
1367 after.value = after_value;
1368 undo_manager_.Push(std::make_unique<OverworldMapPropertyEditAction>(
1369 before, after,
1370 [this](const OverworldPropertyEdit& replay) {
1371 return this->ApplyOverworldPropertyEdit(replay, false);
1372 },
1374 return absl::OkStatus();
1375}
1376
1378 const std::vector<OverworldPropertyEdit>& edits,
1379 const std::string& description, bool record_undo) {
1381 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1382 }
1383 if (edits.empty()) {
1384 return absl::OkStatus();
1385 }
1386
1387 if (!record_undo) {
1388 for (const auto& edit : edits) {
1389 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1390 }
1391 return absl::OkStatus();
1392 }
1393
1395
1396 std::vector<OverworldPropertyEdit> before_edits;
1397 std::vector<OverworldPropertyEdit> after_edits;
1398 before_edits.reserve(edits.size());
1399 after_edits.reserve(edits.size());
1400
1401 for (const auto& edit : edits) {
1402 if (!map_properties_system_->CheckPropertyEditSupported(edit).ok()) {
1403 continue;
1404 }
1405
1406 auto before_value = map_properties_system_->ReadPropertyValue(edit);
1407 if (!before_value.ok()) {
1408 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1409 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1410 }
1411 return before_value.status();
1412 }
1413
1414 OverworldPropertyEdit before = edit;
1415 before.value = *before_value;
1416 const absl::Status apply_status =
1417 map_properties_system_->ApplyPropertyEditDirect(edit);
1418 if (!apply_status.ok()) {
1419 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1420 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1421 }
1422 return apply_status;
1423 }
1424
1425 auto after_value = map_properties_system_->ReadPropertyValue(edit);
1426 if (!after_value.ok()) {
1427 (void)map_properties_system_->ApplyPropertyEditDirect(before);
1428 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1429 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1430 }
1431 return after_value.status();
1432 }
1433 if (*after_value == *before_value) {
1434 continue;
1435 }
1436
1437 OverworldPropertyEdit after = edit;
1438 after.value = *after_value;
1439 before_edits.push_back(std::move(before));
1440 after_edits.push_back(std::move(after));
1441 }
1442
1443 if (after_edits.empty()) {
1444 return absl::OkStatus();
1445 }
1446
1447 const std::string action_description =
1448 description.empty()
1449 ? absl::StrFormat("Edit %d overworld map properties",
1450 static_cast<int>(after_edits.size()))
1451 : description;
1452 undo_manager_.Push(std::make_unique<OverworldMapPropertyBatchEditAction>(
1453 before_edits, after_edits,
1454 [this](const OverworldPropertyEdit& replay) {
1455 return this->ApplyOverworldPropertyEdit(replay, false);
1456 },
1457 action_description));
1458 return absl::OkStatus();
1459}
1460
1462 const std::string& type, int id, const std::string& label) {
1463 if (!dependencies_.project) {
1464 return absl::FailedPreconditionError("No project is open");
1465 }
1466
1468 const std::string before =
1472 const std::string after =
1474 if (after == before) {
1475 return absl::OkStatus();
1476 }
1477
1478 undo_manager_.Push(std::make_unique<OverworldProjectLabelEditAction>(
1479 before, after,
1480 [this, type, id](const std::string& value) {
1482 value);
1483 },
1484 absl::StrFormat("Rename %s 0x%02X", type, id)));
1485 return absl::OkStatus();
1486}
1487
1489 // Finalize any pending paint operation first
1491 return undo_manager_.Undo();
1492}
1493
1495 return undo_manager_.Redo();
1496}
1497
1498// ============================================================================
1499
1501 gfx::ScopedTimer timer("LoadGraphics");
1502
1503 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1504 // Load the Link to the Past overworld.
1505 {
1506 gfx::ScopedTimer load_timer("Overworld::Load");
1508 }
1510
1511 // Fix: Set transparency for the first color of each 16-color subpalette
1512 // This ensures the background color (backdrop) shows through
1513 for (size_t i = 0; i < palette_.size(); i += 16) {
1514 if (i < palette_.size()) {
1515 palette_[i].set_transparent(true);
1516 }
1517 }
1518
1519 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1520
1521 // Phase 1: Create bitmaps without textures for faster loading
1522 // This avoids blocking the main thread with GPU texture creation
1523 {
1524 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1530 }
1531
1532 LOG_DEBUG("OverworldEditor",
1533 "Loading overworld tileset (deferred textures).");
1534 {
1535 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1536 tile16_blockset_bmp_.Create(0x80, 0x2000, 0x08,
1541 }
1542 map_blockset_loaded_ = true;
1543
1544 // Copy the tile16 data into individual tiles.
1545 const auto& tile16_blockset_data = overworld_.tile16_blockset_data();
1546 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
1547
1548 {
1549 gfx::ScopedTimer tilemap_timer("CreateTilemap");
1551 gfx::CreateTilemap(renderer_, tile16_blockset_data, 0x80, 0x2000,
1553
1554 // Queue texture creation for the tile16 blockset atlas
1559 }
1560 }
1561
1562 // Phase 2: reset map bitmaps and leave them fully on-demand.
1563 // The canvas/navigation path will materialize only the visible/current maps.
1564 if (map_texture_) {
1565 map_texture_->ResetMapBitmaps();
1566 }
1567
1568 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
1569 {
1570 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
1572 }
1573 }
1574
1575 return absl::OkStatus();
1576}
1577
1579 // Render the sprites for each Overworld map
1580 const int depth = 0x10;
1581 for (int i = 0; i < 3; i++)
1582 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
1583 int width = sprite.width();
1584 int height = sprite.height();
1585 if (width == 0 || height == 0) {
1586 continue;
1587 }
1588 if (sprite_previews_.size() < sprite.id()) {
1589 sprite_previews_.resize(sprite.id() + 1);
1590 }
1591 sprite_previews_[sprite.id()].Create(width, height, depth,
1592 *sprite.preview_graphics());
1593 sprite_previews_[sprite.id()].SetPalette(palette_);
1596 &sprite_previews_[sprite.id()]);
1597 }
1598 return absl::OkStatus();
1599}
1600
1602 if (map_texture_)
1603 map_texture_->ProcessDeferredTextures();
1604}
1605
1607 if (map_texture_)
1608 map_texture_->EnsureMapTexture(map_index);
1609}
1610
1613 ctx.overworld = &overworld_;
1614 ctx.maps_bmp = &maps_bmp_;
1615 ctx.renderer = renderer_;
1618 ctx.refresh_map_on_demand = [this](int map_index) {
1619 this->RefreshOverworldMapOnDemand(map_index);
1620 };
1621 map_texture_ = std::make_unique<OverworldMapTextureCoordinator>(ctx);
1622}
1623
1626 ctx.rom = rom_;
1627 ctx.overworld = &overworld_;
1628 ctx.maps_bmp = &maps_bmp_;
1632 ctx.palette = &palette_;
1633 ctx.renderer = renderer_;
1638 ctx.game_state = &game_state_;
1640 ctx.status = &status_;
1641 ctx.ensure_map_texture = [this](int map_index) {
1642 this->EnsureMapTexture(map_index);
1643 };
1644 map_refresh_ = std::make_unique<MapRefreshCoordinator>(ctx);
1645}
1646
1648 if (canvas_nav_)
1649 canvas_nav_->HandleMapInteraction();
1650}
1651
1653 if (canvas_nav_)
1654 canvas_nav_->ScrollBlocksetCanvasToCurrentTile();
1655}
1656
1658 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1659 return;
1660 }
1661
1662 if (!rom_ || !rom_->is_loaded() || !all_gfx_loaded_ ||
1664 current_tile16_ = tile_id;
1666 return;
1667 }
1668
1670
1671 if (tile_id == tile16_editor_.current_tile16()) {
1672 current_tile16_ = tile_id;
1673 auto status = tile16_editor_.SetCurrentTile(tile_id);
1674 if (!status.ok()) {
1675 LOG_WARN("OverworldEditor", "Failed to refresh selected Tile16 %d: %s",
1676 tile_id, status.message().data());
1677 }
1678 return;
1679 }
1680
1681 const int from_tile = tile16_editor_.current_tile16();
1682 const bool had_staged_on_current = tile16_editor_.is_tile_modified(from_tile);
1683 if (had_staged_on_current && dependencies_.window_manager) {
1684 const size_t session_id =
1690 }
1691
1694}
1695
1698
1699 // Unregister palette listener
1700 if (palette_listener_id_ >= 0) {
1703 }
1704
1706 current_graphics_set_.clear();
1707 all_gfx_loaded_ = false;
1708 map_blockset_loaded_ = false;
1709 return absl::OkStatus();
1710}
1711
1713 if (canvas_nav_)
1714 canvas_nav_->UpdateBlocksetSelectorState();
1715}
1716
1718 const int next =
1719 std::clamp(current_tile16_ + delta, 0, zelda3::kNumTile16Individual - 1);
1721}
1722
1724 if (!status_bar)
1725 return;
1726 status_bar->SetCustomSegment(
1727 "Map",
1731 status_bar->SetCustomSegment("Tile16",
1732 absl::StrFormat("0x%03X", current_tile16_));
1733
1734 const char* mode_label = "Draw";
1735 switch (entity_edit_mode_) {
1737 mode_label = "Entrances";
1738 break;
1740 mode_label = "Exits";
1741 break;
1743 mode_label = "Items";
1744 break;
1746 mode_label = "Sprites";
1747 break;
1749 mode_label = "Transports";
1750 break;
1752 mode_label = "Music";
1753 break;
1755 switch (current_mode) {
1756 case EditingMode::MOUSE:
1757 mode_label = "Mouse";
1758 break;
1760 mode_label = "Draw";
1761 break;
1763 mode_label = "Fill";
1764 break;
1765 }
1766 break;
1767 }
1768 status_bar->SetEditorMode(mode_label);
1769}
1770
1771} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:157
bool is_loaded() const
Definition rom.h:155
static Flags & get()
Definition features.h:119
bool loaded() const
Check if the manifest has been loaded.
UndoManager undo_manager_
Definition editor.h:339
EditorDependencies dependencies_
Definition editor.h:338
EditorType type() const
Definition editor.h:311
void SetHostSurfaceHint(std::string hint)
Main UI class for editing overworld maps in A Link to the Past.
std::unique_ptr< UsageStatisticsCard > usage_stats_card_
absl::Status Clear() override
std::unique_ptr< MapPropertiesSystem > map_properties_system_
std::unique_ptr< OverworldCanvasRenderer > canvas_renderer_
OverworldEntityWorkbench * GetWorkbench()
Resolve the entity workbench window content (may be null).
zelda3::OverworldEntranceTileTypes entrance_tiletypes_
void HandleTile16Edit()
Handle tile16 editing from context menu (MOUSE mode) Gets the tile16 under the cursor and opens the T...
std::optional< OverworldUndoPoint > current_paint_operation_
void PushItemUndoAction(OverworldItemsSnapshot before, std::string description)
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)
void RestoreItemUndoSnapshot(const OverworldItemsSnapshot &snapshot)
std::vector< int > selected_tile16_ids_
absl::Status ApplyOverworldPropertyEdit(const OverworldPropertyEdit &edit, bool record_undo=true)
void InitCanvasNavigationManager()
Initialize the canvas navigation manager (called during Initialize)
void InitMapRefreshCoordinator()
Initialize the map refresh coordinator (called during Initialize)
std::optional< zelda3::OverworldItem > selected_item_identity_
std::unique_ptr< zelda3::OverworldUpgradeSystem > upgrade_system_
void NotifyEntityModified(zelda3::GameEntity *entity)
Notify that an entity has been modified, marking ROM as dirty.
void HandleEntityInsertion(const std::string &entity_type)
Handle entity insertion from context menu.
Definition automation.cc:75
std::unique_ptr< MapRefreshCoordinator > map_refresh_
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > maps_bmp_
std::unique_ptr< OverworldMapTextureCoordinator > map_texture_
static bool NormalizeMapSelection(int &current_world, int &current_map)
Clamp a possibly stale world/map selection back into a valid overworld range.
void CheckForOverworldEdits()
Check for tile edits - delegates to TilePaintingManager.
void ContributeStatus(StatusBar *status_bar) override
void RefreshSiblingMapGraphics(int map_index, bool include_self=false)
void PrimeWorldMaps(int world, bool process_texture_queue=false)
absl::Status ApplyOverworldPropertyEdits(const std::vector< OverworldPropertyEdit > &edits, const std::string &description, bool record_undo=true)
void RefreshOverworldMapOnDemand(int map_index)
std::unique_ptr< OverworldSidebar > sidebar_
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.
std::optional< int > pending_tile16_selection_after_gfx_
absl::Status RenameProjectResourceLabelWithUndo(const std::string &type, int id, const std::string &label)
absl::Status SaveCurrentSelectionToScratch()
zelda3::OverworldItem * GetSelectedItem()
Resolve selected item identity to the current live item pointer.
bool NormalizeCurrentSelectionState()
Clamp and synchronize stale map/world selection before panels draw.
bool DuplicateSelectedItem(int offset_x=16, int offset_y=0)
Duplicate selected item with optional pixel offset.
void DrawOverworldCanvas()
Draw the main overworld canvas.
zelda3::Overworld & overworld()
Access the underlying Overworld data.
void SetCurrentEntity(zelda3::GameEntity *entity)
Set the currently active entity for editing.
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< CanvasNavigationManager > canvas_nav_
bool NudgeSelectedItem(int delta_x, int delta_y)
Move selected item by signed pixel deltas.
std::unique_ptr< OverworldToolbar > toolbar_
void ProcessDeferredTextures()
Create textures for deferred map bitmaps on demand.
std::unique_ptr< gui::TileSelectorWidget > blockset_selector_
absl::Status Paste() override
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset canvas to show the current selected tile16.
static constexpr auto kPaintBatchTimeout
bool DeleteSelectedItem()
Delete selected item and preserve nearest-item selection continuity.
std::unique_ptr< TilePaintingManager > tile_painting_
absl::Status LoadGraphics()
Load the Bitmap objects for each OverworldMap.
OverworldItemsSnapshot CaptureItemUndoSnapshot() const
std::unique_ptr< DebugWindowCard > debug_window_card_
void SelectMapForEditing(int map_id, bool respect_pin=true)
std::unique_ptr< OverworldInteractionCoordinator > interaction_coordinator_
zelda3::GameEntity * current_entity_
void HandleKeyboardShortcuts()
Handle overworld keyboard shortcuts and edit-mode hotkeys.
std::unique_ptr< EntityMutationService > entity_mutation_service_
void ClearSelectedItem()
Clear active item selection.
void InitTilePaintingManager()
Initialize the tile painting manager (called after graphics load)
bool SelectItemByIdentity(const zelda3::OverworldItem &item_identity)
Select an overworld item using value identity matching.
Authoritative component for entity editing state and UI.
void OpenContextMenuFor(zelda3::GameEntity *entity)
void SetActiveEntity(zelda3::GameEntity *entity)
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:57
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
absl::Status SetCurrentTile(int id)
void set_on_changes_committed(std::function< absl::Status(const std::vector< Tile16Commit > &)> callback)
bool has_pending_changes() const
Check if any tiles have uncommitted changes.
void RequestTileSwitch(int target_tile_id)
absl::Status Initialize(gfx::Bitmap &tile16_blockset_bmp, gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
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_current_tile_changed(std::function< void(int)> callback)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
bool OpenWindow(size_t session_id, const std::string &base_window_id)
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
bool ToggleWindow(size_t session_id, const std::string &base_window_id)
void RegisterPanelAlias(const std::string &legacy_base_id, const std::string &canonical_base_id)
Register a legacy panel ID alias that resolves to a canonical ID.
void MarkWindowRecentlyUsed(const std::string &window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:574
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:581
static Arena & Get()
Definition arena.cc:24
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:202
bool is_active() const
Definition bitmap.h:407
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
SDL_Surface * surface() const
Definition bitmap.h:402
RAII timer for automatic timing management.
auto selected_tile_pos() const
Definition canvas.h:401
auto global_scale() const
Definition canvas.h:403
auto select_rect_active() const
Definition canvas.h:399
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:301
auto drawn_tile_position() const
Definition canvas.h:362
void ClearContextMenuItems()
Definition canvas.cc:874
bool IsMouseHovering() const
Definition canvas.h:344
const ImVector< ImVec2 > & points() const
Definition canvas.h:350
auto selected_points() const
Definition canvas.h:464
Base class for all overworld and dungeon entities.
Definition common.h:31
enum yaze::zelda3::GameEntity::EntityType entity_type_
Represents an overworld exit that transitions from dungeon to overworld.
absl::Status SaveMap32Expanded()
Save expanded tile32 definitions (v1+ ROMs)
void set_current_world(int world)
Definition overworld.h:735
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:131
const std::vector< uint8_t > & current_graphics() const
Definition overworld.h:689
absl::Status SaveMapProperties()
Save per-area graphics, palettes, and messages.
std::vector< std::pair< uint32_t, uint32_t > > GetProjectedWriteRanges() const
Get the projected write ranges (PC offsets) for overworld map saves.
absl::Status SaveMap32Tiles()
Save tile32 definitions to ROM.
const gfx::SnesPalette & current_area_palette() const
Definition overworld.h:705
absl::Status SaveMap16Tiles()
Save tile16 definitions to ROM.
auto all_items() const
Definition overworld.h:756
auto is_loaded() const
Definition overworld.h:728
absl::Status CreateTile32Tilemap()
Build tile32 tilemap from current tile16 data.
auto overworld_map(int i) const
Definition overworld.h:662
const std::vector< uint8_t > & tile16_blockset_data() const
Definition overworld.h:717
absl::Status SaveCustomOverworldData()
Save all v2/v3 property tables using the ROM's current feature flags.
void set_current_map(int i)
Definition overworld.h:734
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.
absl::Status SaveItems()
Save hidden overworld items to ROM.
uint16_t GetTile(int x, int y) const
Definition overworld.h:736
absl::Status SaveOverworldMaps()
Save compressed map tile data to ROM.
auto mutable_sprites(int state)
Definition overworld.h:684
absl::Status SaveMap16Expanded()
Save expanded tile16 definitions (v1+ ROMs)
absl::Status SaveMusic()
Save per-area music IDs.
#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_LAYERS
Definition icons.h:1068
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:110
#define LOG_WARN(category, format,...)
Definition log.h:108
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
bool ItemIdentityMatchesForUndo(const zelda3::OverworldItem &lhs, const zelda3::OverworldItem &rhs)
bool ItemSnapshotsEqual(const OverworldItemsSnapshot &lhs, const OverworldItemsSnapshot &rhs)
Editors are the view controllers for the application.
absl::Status RenameProjectResourceLabel(project::YazeProject *project, const std::string &type, int id, const std::string &label)
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
absl::Status RemoveItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity)
Remove an item by value identity instead of pointer identity.
constexpr unsigned int kOverworldMapSize
absl::Status NudgeItem(zelda3::OverworldItem *item, int delta_x, int delta_y)
Move an item by pixel deltas with overworld bounds clamping.
zelda3::OverworldItem * FindNearestItemForSelection(zelda3::Overworld *overworld, const zelda3::OverworldItem &anchor_identity)
Find the best next item to keep selection continuity after deletion.
zelda3::OverworldItem * FindItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity)
Find a live item by value identity.
std::string DescribeOverworldPropertyEdit(const OverworldPropertyEdit &edit)
constexpr int kTile16Size
int EffectiveOverworldHoverMap(int hovered_map, bool canvas_hovered)
std::string FormatOverworldMapStatusSegment(int current_map, int hovered_map)
std::string GetProjectResourceLabel(const project::YazeProject *project, const std::string &type, int id)
absl::StatusOr< zelda3::OverworldItem * > DuplicateItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity, int offset_x, int offset_y)
Duplicate an existing item by identity with a positional offset.
Tilemap CreateTilemap(IRenderer *renderer, const std::vector< uint8_t > &data, int width, int height, int tile_size, int num_tiles, const SnesPalette &palette)
Definition tilemap.cc:14
EmptyStateOptions EmptyLoading(const char *what, bool compact)
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
EmptyStateOptions EmptyNoRom(bool compact)
constexpr int kNumTile16Individual
Definition overworld.h:241
constexpr int kNumOverworldMaps
Definition common.h:85
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:89
OverworldRomProfile DetectOverworldRomProfile(const Rom &rom)
Definition overworld.h:311
absl::StatusOr< OverworldEntranceTileTypes > LoadEntranceTileTypes(Rom *rom)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Overworld overworld
Callbacks for operations that remain in the OverworldEditor.
std::function< absl::Status()> refresh_tile16_blockset
std::function< void(int, bool)> select_map_for_editing
std::function< bool()> is_entity_hovered
Returns true if an entity is currently hovered (for pan suppression).
Shared state pointers that the navigation manager reads/writes.
std::unique_ptr< gui::TileSelectorWidget > * blockset_selector
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
project::YazeProject * project
Definition editor.h:173
SharedClipboard * shared_clipboard
Definition editor.h:186
gfx::IRenderer * renderer
Definition editor.h:191
WorkspaceWindowManager * window_manager
Definition editor.h:181
Context struct holding all data dependencies for map refresh operations. All pointers/references must...
std::function< void(int map_index)> ensure_map_texture
Callback to ensure a map texture is created (stays in OverworldEditor)
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
std::function< void(int map_index)> refresh_map_on_demand
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
Sink for high-level commands emitted by the interaction coordinator.
std::function< void(zelda3::GameEntity *) on_entity_double_click)
std::function< void(zelda3::GameEntity *) on_entity_context_menu)
std::function< void(int, int, bool)> on_nudge_selected
std::function< void(EditingMode)> on_set_editor_mode
std::function< void(EntityEditMode)> on_set_entity_mode
Snapshot of overworld item list + current item selection.
std::vector< zelda3::OverworldItem > items
std::optional< zelda3::OverworldItem > selected_item_identity
static constexpr const char * kItemList
static constexpr const char * kTile16Editor
std::vector< int > overworld_tile16_ids
Definition editor.h:119
Callbacks for undo integration and map refresh.
std::function< void(int tile_id)> request_tile16_selection
When set, eyedropper routes here (guarded RequestTileSwitch path).
std::function< void(int map_index)> refresh_overworld_map_on_demand
std::function< void()> scroll_blockset_to_current_tile
std::function< void()> finalize_paint_operation
std::function< void(int map_id, int world, int x, int y, int old_tile_id)> create_undo_point
Shared state for the tile painting system.
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
RomWritePolicy write_policy
Definition project.h:110
core::HackManifest hack_manifest
Definition project.h:212