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
45// Note: All overworld panels now self-register via REGISTER_PANEL macro:
46// AreaGraphicsPanel, DebugWindowPanel, GfxGroupsPanel, MapPropertiesPanel,
47// OverworldCanvasPanel, ScratchSpacePanel, Tile16EditorPanel, Tile16SelectorPanel,
48// Tile8SelectorPanel, UsageStatisticsPanel, V3SettingsPanel, OverworldItemListPanel
56#include "app/gfx/core/bitmap.h"
66#include "app/gui/core/icons.h"
68#include "app/gui/core/style.h"
72#include "core/asar_wrapper.h"
73#include "core/features.h"
74#include "core/project.h"
75#include "rom/rom.h"
76#include "util/file_util.h"
77#include "util/hex.h"
78#include "util/log.h"
79#include "util/macro.h"
80#include "zelda3/common.h"
88
89namespace yaze::editor {
90
91namespace {
92
94 const zelda3::OverworldItem& rhs) {
95 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
96 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.bg2_ == rhs.bg2_;
97}
98
100 const OverworldItemsSnapshot& rhs) {
101 if (lhs.items.size() != rhs.items.size()) {
102 return false;
103 }
104
105 for (size_t i = 0; i < lhs.items.size(); ++i) {
106 if (!ItemIdentityMatchesForUndo(lhs.items[i], rhs.items[i])) {
107 return false;
108 }
109 }
110
111 if (lhs.selected_item_identity.has_value() !=
112 rhs.selected_item_identity.has_value()) {
113 return false;
114 }
115 if (!lhs.selected_item_identity.has_value()) {
116 return true;
117 }
118
121}
122
123} // namespace
124
126 if (palette_listener_id_ >= 0) {
129 }
130
137
138 // These store callbacks and raw pointers into the editor. Release them while
139 // all editor-owned state is still alive rather than during member teardown.
140 sidebar_.reset();
142}
143
145 int& current_map) {
146 const int clamped_world = std::clamp(current_world, 0, 2);
147 int normalized_map = current_map;
148 if (normalized_map < 0 || normalized_map >= zelda3::kNumOverworldMaps) {
149 normalized_map =
150 std::min(clamped_world * 0x40, zelda3::kNumOverworldMaps - 1);
151 }
152
153 const int normalized_world = std::clamp(normalized_map / 0x40, 0, 2);
154 const bool changed =
155 normalized_world != current_world || normalized_map != current_map;
156 current_world = normalized_world;
157 current_map = normalized_map;
158 return changed;
159}
160
162 // Initialize renderer from dependencies
164
166 "Gfx Groups: selection syncs with the Graphics editor for this ROM "
167 "session "
168 "(this surface uses its own preview canvases).");
169
171
172 // Initialize MapRefreshCoordinator (must be before callbacks that use it)
174
175 if (rom_) {
176 upgrade_system_ = std::make_unique<zelda3::OverworldUpgradeSystem>(*rom_);
177 }
179 std::make_unique<EntityMutationService>(overworld_);
180
182
183 // Note: All WindowContent instances now self-register via REGISTER_PANEL macro
184 // and use ContentRegistry::Context to access the current editor.
185 // See comment at include section for the current set of view wrappers.
186
187 // Original initialization code below:
188 // Initialize MapPropertiesSystem with canvas and bitmap data
189 // Initialize cards
190 usage_stats_card_ = std::make_unique<UsageStatisticsCard>(&overworld_);
191 debug_window_card_ = std::make_unique<DebugWindowCard>();
192
193 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
195
196 // Set up refresh callbacks for MapPropertiesSystem
197 map_properties_system_->SetRefreshCallbacks(
198 [this]() { this->RefreshMapProperties(); },
199 [this]() { this->RefreshOverworldMap(); },
200 [this]() -> absl::Status { return this->RefreshMapPalette(); },
201 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
202 [this](int map_index) { this->ForceRefreshGraphics(map_index); });
203 map_properties_system_->SetMapSelectionCallback(
204 [this](int map_index, bool respect_pin) {
205 this->SelectMapForEditing(map_index, respect_pin);
206 });
207 map_properties_system_->SetPropertyEditCallback(
208 [this](const OverworldPropertyEdit& edit) {
209 return this->ApplyOverworldPropertyEdit(edit);
210 });
211 map_properties_system_->SetPropertyEditBatchCallback(
212 [this](const std::vector<OverworldPropertyEdit>& edits,
213 const std::string& description) {
214 return this->ApplyOverworldPropertyEdits(edits, description);
215 });
216 map_properties_system_->SetResourceLabelEditCallback(
217 [this](const std::string& type, int id, const std::string& label) {
218 return this->RenameProjectResourceLabelWithUndo(type, id, label);
219 });
220
221 // Initialize OverworldSidebar
222 sidebar_ = std::make_unique<OverworldSidebar>(&overworld_, rom_,
224
225 // Initialize OverworldEntityRenderer for entity visualization
226 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
228
229 // Initialize Toolbar
230 toolbar_ = std::make_unique<OverworldToolbar>();
231 toolbar_->on_world_changed = [this](int world) {
232 SwitchToWorld(world);
233 };
234 toolbar_->on_refresh_graphics = [this]() {
235 // Invalidate cached graphics for the current map area to force re-render
236 // with potentially new palette/graphics settings
239 };
240 toolbar_->on_refresh_map = [this]() {
242 };
243
244 toolbar_->on_save_to_scratch = [this]() {
246 };
247 toolbar_->on_load_from_scratch = [this]() {
249 };
250 toolbar_->on_upgrade_rom_version = [this](int) {
251 ImGui::OpenPopup("UpgradeROMVersion");
252 };
253 toolbar_->on_apply_property_edit = [this](const OverworldPropertyEdit& edit) {
254 return this->ApplyOverworldPropertyEdit(edit);
255 };
256 toolbar_->on_rename_resource_label = [this](const std::string& type, int id,
257 const std::string& label) {
258 return this->RenameProjectResourceLabelWithUndo(type, id, label);
259 };
260
261 // Initialize OverworldCanvasRenderer for canvas and panel drawing
262 canvas_renderer_ = std::make_unique<OverworldCanvasRenderer>(this);
263
267}
268
271 sink.on_set_editor_mode = [this](EditingMode mode) {
272 current_mode = mode;
275 } else {
277 }
278 };
279 sink.on_set_entity_mode = [this](EntityEditMode mode) {
280 entity_edit_mode_ = mode;
283 };
284 sink.on_toggle_brush = [this]() {
286 };
287 sink.on_activate_fill = [this]() {
289 };
290 sink.on_pick_tile_from_hover = [this]() {
292 };
293 sink.on_duplicate_selected = [this]() {
294 (void)DuplicateSelectedItem();
295 };
296 sink.on_nudge_selected = [this](int delta_x, int delta_y, bool shift_held) {
297 const int step = shift_held ? 16 : 1;
298 (void)this->NudgeSelectedItem(delta_x * step, delta_y * step);
299 };
300 sink.on_entity_context_menu = [this](zelda3::GameEntity* entity) {
302 auto* workbench = static_cast<OverworldEntityWorkbench*>(
304 "overworld.entity_workbench"));
305 if (workbench) {
306 workbench->OpenContextMenuFor(entity);
307 }
308 }
309 };
310 sink.on_entity_double_click = [this](zelda3::GameEntity* entity) {
311 if (!entity) {
312 return;
313 }
314 switch (entity->entity_type_) {
316 this->jump_to_tab_ =
317 static_cast<zelda3::OverworldExit*>(entity)->room_id_;
318 break;
320 this->jump_to_tab_ =
321 static_cast<zelda3::OverworldEntrance*>(entity)->entrance_id_;
322 break;
323 default:
324 break;
325 }
326 };
327 sink.on_toggle_item_list = [this]() {
329 return;
330 }
331 const size_t session_id =
335 };
336 sink.can_edit_items = [this]() {
338 };
339 sink.on_toggle_lock = [this]() {
341 };
342 sink.on_toggle_tile16_editor = [this]() {
344 return;
345 }
346 const size_t session_id =
350 };
351 sink.on_toggle_fullscreen = [this]() {
353 };
354 sink.on_undo = [this]() {
355 status_ = Undo();
356 };
357 sink.on_redo = [this]() {
358 status_ = Redo();
359 };
361 std::make_unique<OverworldInteractionCoordinator>(std::move(sink));
362}
363
367 deps.overworld = &overworld_;
368 deps.maps_bmp = &maps_bmp_;
376 deps.game_state = &game_state_;
377 deps.rom = rom_;
379
380 TilePaintingCallbacks callbacks;
381 callbacks.create_undo_point = [this](int map_id, int world, int x, int y,
382 int old_tile_id) {
383 this->CreateUndoPoint(map_id, world, x, y, old_tile_id);
384 };
385 callbacks.finalize_paint_operation = [this]() {
387 };
388 callbacks.refresh_overworld_map = [this]() {
389 this->RefreshOverworldMap();
390 };
391 callbacks.refresh_overworld_map_on_demand = [this](int map_index) {
392 this->RefreshOverworldMapOnDemand(map_index);
393 };
394 callbacks.scroll_blockset_to_current_tile = [this]() {
396 };
397 callbacks.request_tile16_selection = [this](int tile_id) {
398 this->RequestTile16Selection(tile_id);
399 };
400
401 tile_painting_ = std::make_unique<TilePaintingManager>(deps, callbacks);
402}
403
407 ctx.overworld = &overworld_;
408 ctx.rom = rom_;
418 ctx.maps_bmp = &maps_bmp_;
421
423 callbacks.refresh_overworld_map = [this]() {
424 this->RefreshOverworldMap();
425 };
426 callbacks.refresh_tile16_blockset = [this]() -> absl::Status {
427 return this->RefreshTile16Blockset();
428 };
429 callbacks.ensure_map_texture = [this](int map_index) {
430 this->EnsureMapTexture(map_index);
431 };
432 callbacks.select_map_for_editing = [this](int map_index, bool respect_pin) {
433 this->SelectMapForEditing(map_index, respect_pin);
434 };
435 callbacks.pick_tile16_from_hovered_canvas = [this]() -> bool {
436 return this->PickTile16FromHoveredCanvas();
437 };
438 callbacks.is_entity_hovered = [this]() -> bool {
439 return entity_renderer_ && entity_renderer_->hovered_entity() != nullptr;
440 };
441
442 canvas_nav_ = std::make_unique<CanvasNavigationManager>();
443 canvas_nav_->Initialize(ctx, callbacks);
444}
445
447 current_entity_ = entity;
448 if (!entity) {
450 return;
451 }
453 selected_item_identity_ = *static_cast<zelda3::OverworldItem*>(entity);
454 } else {
456 }
457}
458
464
465absl::Status OverworldEditor::Load() {
466 gfx::ScopedTimer timer("OverworldEditor::Load");
467
468 LOG_DEBUG("OverworldEditor", "Loading overworld.");
469 if (!rom_ || !rom_->is_loaded()) {
470 return absl::FailedPreconditionError("ROM not loaded");
471 }
472
474 dependencies_.window_manager->RegisterPanelAlias("overworld.debug_window",
475 "overworld.debug");
476 }
477
478 // Clear undo/redo state when loading new ROM data
481
486
487 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
491 [this](int id) { current_tile16_ = id; });
492
493 // Set up callback for when tile16 changes are committed
495 [this](const std::vector<Tile16Commit>& commits) -> absl::Status {
496 auto* tiles16 = overworld_.mutable_tiles16();
497 for (const auto& commit : commits) {
498 if (commit.tile_id >= 0 &&
499 commit.tile_id < static_cast<int>(tiles16->size())) {
500 (*tiles16)[commit.tile_id] = commit.tile_data;
501 }
502 }
503
504 // Regenerate the overworld editor's tile16 blockset
506
507 // Force refresh of the current overworld map to show changes
509
510 LOG_DEBUG("OverworldEditor",
511 "Overworld editor refreshed after Tile16 changes");
512 return absl::OkStatus();
513 });
514
515 // Set up entity insertion callback for MapPropertiesSystem
517 map_properties_system_->SetEntityCallbacks(
518 [this](const std::string& entity_type) {
519 HandleEntityInsertion(entity_type);
520 });
521
522 // Set up tile16 edit callback for context menu in MOUSE mode
523 map_properties_system_->SetTile16SampleCallback(
524 [this]() { return PickTile16FromHoveredCanvas(); });
525 map_properties_system_->SetTile16EditCallback(
526 [this]() { HandleTile16Edit(); });
527 }
528
530
531 // Register as palette listener to refresh graphics when palettes change
532 if (palette_listener_id_ < 0) {
534 [this](const std::string& group_name, int palette_index) {
535 // Only respond to overworld-related palette changes
536 if (group_name == "ow_main" || group_name == "ow_animated" ||
537 group_name == "ow_aux" || group_name == "grass") {
538 LOG_DEBUG("OverworldEditor",
539 "Palette change detected: %s, refreshing current map",
540 group_name.c_str());
541 // Refresh current map graphics to reflect palette changes
542 if (current_map_ >= 0 && all_gfx_loaded_) {
544 }
545 }
546 });
547 LOG_DEBUG("OverworldEditor", "Registered as palette listener (ID: %d)",
549 }
550
551 all_gfx_loaded_ = true;
552
553 auto scratch_status = LoadScratchPad();
554 if (!scratch_status.ok()) {
555 LOG_WARN("OverworldEditor", "Failed to load scratch pad: %s",
556 std::string(scratch_status.message()).c_str());
557 }
558
560 const int pending = *pending_tile16_selection_after_gfx_;
562 RequestTile16Selection(pending);
563 }
564
565 return absl::OkStatus();
566}
567
569 status_ = absl::OkStatus();
570
571 // Safety check: Ensure ROM is loaded and graphics are ready
572 if (!rom_ || !rom_->is_loaded()) {
573 gui::CenterText("No ROM loaded");
574 return absl::OkStatus();
575 }
576
577 if (!all_gfx_loaded_) {
578 gui::CenterText("Loading graphics...");
579 return absl::OkStatus();
580 }
581
583
584 // Process deferred textures for smooth loading
586
587 // Update blockset atlas with any pending tile16 changes for live preview
588 // Tile cache now uses copy semantics so this is safe to enable
591 }
592
593 // Early return if window_manager is not available
594 // (panels won't be drawn without it, so no point continuing)
596 return status_;
597 }
598
600 return status_;
601 }
602
603 // ===========================================================================
604 // Main Overworld Canvas
605 // ===========================================================================
606 // The panels (Tile16 Selector, Area Graphics, etc.) are now managed by
607 // WindowContent/WorkspaceWindowManager and drawn automatically. This section only
608 // handles the main canvas and toolbar.
609
610 // ===========================================================================
611 // Non-Panel Windows (not managed by WindowContent system)
612 // ===========================================================================
613 // These are separate feature windows, not part of the panel system
614
615 // Custom Background Color Editor
617 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
618 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color",
620 if (rom_->is_loaded() && overworld_.is_loaded() &&
622 map_properties_system_->DrawCustomBackgroundColorEditor(
624 }
625 }
626 ImGui::End();
627 }
628
629 // Visual Effects Editor (Subscreen Overlays)
631 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
632 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor",
634 if (rom_->is_loaded() && overworld_.is_loaded() &&
636 map_properties_system_->DrawOverlayEditor(current_map_,
638 }
639 }
640 ImGui::End();
641 }
642
643 // Note: Tile16 Editor is now managed as an WindowContent (Tile16EditorPanel)
644 // It uses UpdateAsPanel() which provides a context menu instead of MenuBar
645
646 if (auto* workbench = GetWorkbench()) {
647 workbench->ProcessPendingInsertion(entity_mutation_service_.get(),
649 workbench->DrawPopups();
650 }
651
652 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr,
653 ImGuiWindowFlags_AlwaysAutoResize)) {
654 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
655 ImGui::Separator();
656 ImGui::TextWrapped(
657 tr("This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
658 "enabling advanced overworld features."));
659 ImGui::Separator();
660
661 const uint8_t current_version =
663 : 0xFF;
664 ImGui::Text(tr("Current Version: %s"),
665 current_version == 0xFF
666 ? "Vanilla"
667 : absl::StrFormat("v%d", current_version).c_str());
668
669 static int target_version = 3;
670 ImGui::RadioButton(tr("v2 (Basic features)"), &target_version, 2);
671 ImGui::SameLine();
672 ImGui::RadioButton(tr("v3 (All features)"), &target_version, 3);
673
674 ImGui::Separator();
675
676 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150.0f, 0.0f))) {
677 auto upgrade_status =
678 upgrade_system_ != nullptr
679 ? upgrade_system_->ApplyZSCustomOverworldASM(target_version)
680 : absl::FailedPreconditionError("Upgrade system not initialized");
681 if (upgrade_status.ok()) {
682 status_ = Clear();
683 if (status_.ok()) {
684 status_ = Load();
685 }
686 if (status_.ok()) {
687 ImGui::CloseCurrentPopup();
688 }
689 } else {
690 LOG_ERROR("OverworldEditor", "Upgrade failed: %s",
691 upgrade_status.message().data());
692 }
693 }
694 ImGui::SameLine();
695 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150.0f, 0.0f))) {
696 ImGui::CloseCurrentPopup();
697 }
698
699 ImGui::EndPopup();
700 }
701
702 // ===========================================================================
703 // Centralized Entity Interaction Logic (extracted to dedicated method)
704 // ===========================================================================
705 zelda3::GameEntity* hovered_entity =
706 entity_renderer_ ? entity_renderer_->hovered_entity() : nullptr;
708 interaction_coordinator_->Update(hovered_entity);
709 }
710
711 return absl::OkStatus();
712}
713
716
717 if (canvas_renderer_) {
718 canvas_renderer_->DrawOverworldCanvas();
719 }
720}
721
724
727 if (const auto* map = overworld_.overworld_map(current_map_)) {
728 current_parent_ = map->parent();
729 } else {
731 }
732
733 if (changed) {
734 LOG_WARN("OverworldEditor",
735 "Normalized stale overworld selection to world=%d map=%d",
737 }
738 return changed;
739}
740
741void OverworldEditor::SelectMapForEditing(int map_id, bool respect_pin) {
742 if (respect_pin && current_map_lock_) {
743 return;
744 }
745 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
746 return;
747 }
748 const auto* map = overworld_.overworld_map(map_id);
749 if (!map) {
750 return;
751 }
752
754 current_map_ = map_id;
755 current_world_ = std::clamp(map_id / 0x40, 0, 2);
756 current_parent_ = map->parent();
759
761 if (!status_.ok()) {
763 return;
764 }
765
767 if (all_gfx_loaded_) {
770 }
771}
772
773void OverworldEditor::PrimeWorldMaps(int world, bool process_texture_queue) {
774 if (map_texture_)
775 map_texture_->PrimeWorldMaps(world, process_texture_queue);
776}
777
780
781 const int clamped_world = std::clamp(world, 0, 2);
782 const int local_map = current_map_ & 0x3F;
783 const int world_start = clamped_world * 0x40;
784 const int maps_remaining = zelda3::kNumOverworldMaps - world_start;
785 if (maps_remaining <= 0) {
786 return;
787 }
788
789 hovered_map_ = -1;
790 SelectMapForEditing(world_start + std::min(local_map, maps_remaining - 1),
791 false);
793
795 if (!status_.ok()) {
797 return;
798 }
799
805}
806
812
814 const zelda3::OverworldItem& item_identity) {
815 auto* item = FindItemByIdentity(&overworld_, item_identity);
816 if (!item) {
817 return false;
818 }
819
821 current_entity_ = item;
823 auto* workbench = static_cast<OverworldEntityWorkbench*>(
825 "overworld.entity_workbench"));
826 if (workbench) {
827 workbench->SetActiveEntity(item);
828 }
829 }
830 return true;
831}
832
835 current_entity_ = nullptr;
837 auto* workbench = static_cast<OverworldEntityWorkbench*>(
839 "overworld.entity_workbench"));
840 if (workbench) {
841 workbench->SetActiveEntity(nullptr);
842 }
843 }
844}
845
848 return nullptr;
849 return static_cast<OverworldEntityWorkbench*>(
851 "overworld.entity_workbench"));
852}
853
855 if (!selected_item_identity_.has_value()) {
856 return nullptr;
857 }
858
860 if (!item) {
862 return nullptr;
863 }
864
865 current_entity_ = item;
866 if (auto* workbench = GetWorkbench()) {
867 workbench->SetActiveEntity(item);
868 }
869 return item;
870}
871
873 return const_cast<OverworldEditor*>(this)->GetSelectedItem();
874}
875
882
884 const OverworldItemsSnapshot& snapshot) {
885 auto* items = overworld_.mutable_all_items();
886 if (!items) {
887 return;
888 }
889
890 *items = snapshot.items;
892 if (selected_item_identity_.has_value()) {
893 auto* selected_item =
895 if (selected_item) {
896 current_entity_ = selected_item;
897 if (auto* workbench = GetWorkbench()) {
898 workbench->SetActiveEntity(selected_item);
899 }
900 } else {
902 }
903 } else {
905 }
906
907 if (rom_) {
908 rom_->set_dirty(true);
909 }
911}
912
914 std::string description) {
916 if (ItemSnapshotsEqual(before, after)) {
917 return;
918 }
919
920 undo_manager_.Push(std::make_unique<OverworldItemsEditAction>(
921 std::move(before), std::move(after),
922 [this](const OverworldItemsSnapshot& snapshot) {
923 RestoreItemUndoSnapshot(snapshot);
924 },
925 std::move(description)));
926}
927
928bool OverworldEditor::DuplicateSelectedItem(int offset_x, int offset_y) {
929 auto* selected_item = GetSelectedItem();
930 if (!selected_item) {
933 "Select an overworld item first (Item Mode: key 5)",
935 }
936 return false;
937 }
938
939 auto before_snapshot = CaptureItemUndoSnapshot();
942 duplicate_result = entity_mutation_service_->DuplicateItem(
943 *selected_item, offset_x, offset_y);
944 } else {
945 auto duplicate_or = DuplicateItemByIdentity(&overworld_, *selected_item,
946 offset_x, offset_y);
947 if (duplicate_or.ok()) {
948 duplicate_result.entity = *duplicate_or;
949 duplicate_result.status = absl::OkStatus();
950 } else {
951 duplicate_result.status = duplicate_or.status();
952 duplicate_result.error_message =
953 "Failed to duplicate overworld item: " +
954 std::string(duplicate_or.status().message());
955 }
956 }
957 if (!duplicate_result.ok()) {
960 duplicate_result.error_message.empty()
961 ? "Failed to duplicate overworld item"
962 : duplicate_result.error_message,
964 }
965 return false;
966 }
967
968 auto* duplicated_item =
969 static_cast<zelda3::OverworldItem*>(duplicate_result.entity);
970 selected_item_identity_ = *duplicated_item;
971 current_entity_ = duplicated_item;
972 if (auto* workbench = GetWorkbench()) {
973 workbench->SetActiveEntity(duplicated_item);
974 }
975 PushItemUndoAction(std::move(before_snapshot),
976 absl::StrFormat("Duplicate overworld item 0x%02X",
977 static_cast<int>(duplicated_item->id_)));
978 rom_->set_dirty(true);
981 absl::StrFormat("Duplicated item 0x%02X",
982 static_cast<int>(duplicated_item->id_)),
984 }
985 return true;
986}
987
988bool OverworldEditor::NudgeSelectedItem(int delta_x, int delta_y) {
989 auto* selected_item = GetSelectedItem();
990 if (!selected_item) {
991 return false;
992 }
993
994 auto before_snapshot = CaptureItemUndoSnapshot();
995 auto status = NudgeItem(selected_item, delta_x, delta_y);
996 if (!status.ok()) {
998 dependencies_.toast_manager->Show("Failed to move selected item",
1000 }
1001 return false;
1002 }
1003
1004 selected_item_identity_ = *selected_item;
1005 current_entity_ = selected_item;
1006 if (auto* workbench = GetWorkbench()) {
1007 workbench->SetActiveEntity(selected_item);
1008 }
1010 std::move(before_snapshot),
1011 absl::StrFormat("Move overworld item 0x%02X (%+d,%+d)",
1012 static_cast<int>(selected_item->id_), delta_x, delta_y));
1013 rom_->set_dirty(true);
1014 return true;
1015}
1016
1018 auto* selected_item = GetSelectedItem();
1019 if (!selected_item) {
1020 return false;
1021 }
1022
1023 auto before_snapshot = CaptureItemUndoSnapshot();
1024 const zelda3::OverworldItem selected_identity = *selected_item;
1025 const uint8_t deleted_item_id = selected_identity.id_;
1026 absl::Status remove_status =
1027 absl::FailedPreconditionError("Entity mutation service not initialized");
1029 remove_status =
1030 entity_mutation_service_->DeleteItem(selected_identity).status;
1031 } else {
1032 remove_status = RemoveItemByIdentity(&overworld_, selected_identity);
1033 }
1034 if (!remove_status.ok()) {
1036 dependencies_.toast_manager->Show("Failed to delete selected item",
1038 }
1039 return false;
1040 }
1041
1042 auto* nearest_item =
1044 ? entity_mutation_service_->ResolveNextSelection(selected_identity)
1045 : FindNearestItemForSelection(&overworld_, selected_identity);
1046 if (nearest_item) {
1047 selected_item_identity_ = *nearest_item;
1048 current_entity_ = nearest_item;
1049 if (auto* workbench = GetWorkbench()) {
1050 workbench->SetActiveEntity(nearest_item);
1051 }
1052 } else {
1054 }
1055
1056 PushItemUndoAction(std::move(before_snapshot),
1057 absl::StrFormat("Delete overworld item 0x%02X",
1058 static_cast<int>(deleted_item_id)));
1059 rom_->set_dirty(true);
1061 if (nearest_item) {
1063 absl::StrFormat("Deleted item 0x%02X (selected nearest 0x%02X)",
1064 static_cast<int>(deleted_item_id),
1065 static_cast<int>(nearest_item->id_)),
1067 } else {
1069 absl::StrFormat("Deleted overworld item 0x%02X",
1070 static_cast<int>(deleted_item_id)),
1072 }
1073 }
1074 return true;
1075}
1076
1078 tile_painting_->CheckForOverworldEdits();
1079}
1080
1083 return absl::FailedPreconditionError("Clipboard unavailable");
1084 }
1085 // If a rectangle selection exists, copy its tile16 IDs into shared clipboard
1087 !ow_map_canvas_.selected_points().empty()) {
1088 std::vector<int> ids;
1089 // selected_points are now stored in world coordinates
1090 const auto start = ow_map_canvas_.selected_points()[0];
1091 const auto end = ow_map_canvas_.selected_points()[1];
1092 const int start_x =
1093 static_cast<int>(std::floor(std::min(start.x, end.x) / 16.0f));
1094 const int end_x =
1095 static_cast<int>(std::floor(std::max(start.x, end.x) / 16.0f));
1096 const int start_y =
1097 static_cast<int>(std::floor(std::min(start.y, end.y) / 16.0f));
1098 const int end_y =
1099 static_cast<int>(std::floor(std::max(start.y, end.y) / 16.0f));
1100 const int width = end_x - start_x + 1;
1101 const int height = end_y - start_y + 1;
1102 ids.reserve(width * height);
1105 for (int y = start_y; y <= end_y; ++y) {
1106 for (int x = start_x; x <= end_x; ++x) {
1107 ids.push_back(overworld_.GetTile(x, y));
1108 }
1109 }
1110
1115 return absl::OkStatus();
1116 }
1117 // Single tile copy fallback
1118 if (current_tile16_ >= 0) {
1123 return absl::OkStatus();
1124 }
1125 return absl::FailedPreconditionError("Nothing selected to copy");
1126}
1127
1130 return absl::FailedPreconditionError("Clipboard unavailable");
1131 }
1133 return absl::FailedPreconditionError("Clipboard empty");
1134 }
1135 if (ow_map_canvas_.points().empty() &&
1137 return absl::FailedPreconditionError("No paste target");
1138 }
1139
1140 // Determine paste anchor position (use current mouse drawn tile position)
1141 // Unscale coordinates to get world position
1142 const ImVec2 scaled_anchor = ow_map_canvas_.drawn_tile_position();
1143 float scale = ow_map_canvas_.global_scale();
1144 if (scale <= 0.0f)
1145 scale = 1.0f;
1146 const ImVec2 anchor =
1147 ImVec2(scaled_anchor.x / scale, scaled_anchor.y / scale);
1148
1149 // Compute anchor in tile16 grid within the current map
1150 const int tile16_x =
1151 (static_cast<int>(anchor.x) % kOverworldMapSize) / kTile16Size;
1152 const int tile16_y =
1153 (static_cast<int>(anchor.y) % kOverworldMapSize) / kTile16Size;
1154
1155 auto& selected_world =
1156 (current_world_ == 0) ? overworld_.mutable_map_tiles()->light_world
1157 : (current_world_ == 1) ? overworld_.mutable_map_tiles()->dark_world
1158 : overworld_.mutable_map_tiles()->special_world;
1159
1160 const int world_offset = current_world_ * 0x40;
1161 const int local_map = current_map_ - world_offset;
1162 const int superY = local_map / 8;
1163 const int superX = local_map % 8;
1164
1168
1169 // Guard
1170 if (width * height != static_cast<int>(ids.size())) {
1171 return absl::InternalError("Clipboard dimensions mismatch");
1172 }
1173
1174 bool any_changed = false;
1175 for (int dy = 0; dy < height; ++dy) {
1176 for (int dx = 0; dx < width; ++dx) {
1177 const int id = ids[dy * width + dx];
1178 const int gx = tile16_x + dx;
1179 const int gy = tile16_y + dy;
1180
1181 const int global_x = superX * 32 + gx;
1182 const int global_y = superY * 32 + gy;
1183 if (global_x < 0 || global_x >= 256 || global_y < 0 || global_y >= 256)
1184 continue;
1185 const int old_tile_id = selected_world[global_x][global_y];
1186 if (old_tile_id == id) {
1187 continue;
1188 }
1189 CreateUndoPoint(current_map_, current_world_, global_x, global_y,
1190 old_tile_id);
1191 selected_world[global_x][global_y] = id;
1192 any_changed = true;
1193 }
1194 }
1195
1196 if (any_changed) {
1198 rom_->set_dirty(true);
1200 }
1201 return absl::OkStatus();
1202}
1203
1205 if (canvas_nav_)
1206 return canvas_nav_->CheckForCurrentMap();
1207 return absl::OkStatus();
1208}
1209
1211 // Delegate to the existing GfxGroupEditor
1212 if (rom_ && rom_->is_loaded()) {
1213 return gfx_group_editor_.Update();
1214 } else {
1215 gui::CenterText("No ROM loaded");
1216 return absl::OkStatus();
1217 }
1218}
1219
1220// DrawV3Settings - now in OverworldCanvasRenderer
1221
1222// DrawMapProperties - now in OverworldCanvasRenderer
1223
1225 // HACK MANIFEST VALIDATION
1226 const bool saving_maps =
1228 if (saving_maps && dependencies_.project &&
1230 const auto& manifest = dependencies_.project->hack_manifest;
1231 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1232
1233 // Calculate memory ranges that would be written by overworld map saves.
1234 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1235 // address space (LoROM), so convert before analysis.
1236 auto ranges = overworld_.GetProjectedWriteRanges();
1238 manifest, write_policy, ranges, "overworld maps", "OverworldEditor",
1240 }
1241
1242 if (saving_maps) {
1243 const auto profile = zelda3::DetectOverworldRomProfile(*rom_);
1245 if (profile.has_expanded_tile32) {
1247 } else {
1249 }
1250 if (profile.has_expanded_tile16) {
1252 } else {
1254 }
1256 }
1257 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1259 }
1260 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1262 }
1263 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1265 }
1266 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1270 }
1271 return absl::OkStatus();
1272}
1273
1274// ============================================================================
1275// Undo/Redo System Implementation
1276// ============================================================================
1277
1279 switch (world) {
1280 case 0:
1281 return overworld_.mutable_map_tiles()->light_world;
1282 case 1:
1283 return overworld_.mutable_map_tiles()->dark_world;
1284 default:
1285 return overworld_.mutable_map_tiles()->special_world;
1286 }
1287}
1288
1289void OverworldEditor::CreateUndoPoint(int map_id, int world, int x, int y,
1290 int old_tile_id) {
1291 auto now = std::chrono::steady_clock::now();
1292
1293 // Check if we should batch with current operation (same map, same world,
1294 // within timeout)
1295 if (current_paint_operation_.has_value() &&
1296 current_paint_operation_->map_id == map_id &&
1297 current_paint_operation_->world == world &&
1299 // Add to existing operation
1300 current_paint_operation_->tile_changes.emplace_back(std::make_pair(x, y),
1301 old_tile_id);
1302 } else {
1303 // Finalize any pending operation before starting a new one
1305
1306 // Start new operation
1308 OverworldUndoPoint{.map_id = map_id,
1309 .world = world,
1310 .tile_changes = {{{x, y}, old_tile_id}},
1311 .timestamp = now};
1312 }
1313
1314 last_paint_time_ = now;
1315}
1316
1318 if (!current_paint_operation_.has_value()) {
1319 return;
1320 }
1321
1322 // Push to the UndoManager (new framework path).
1323 auto& world_tiles = GetWorldTiles(current_paint_operation_->world);
1324 std::vector<OverworldTileChange> changes;
1325 changes.reserve(current_paint_operation_->tile_changes.size());
1326 for (const auto& [coords, old_tile_id] :
1327 current_paint_operation_->tile_changes) {
1328 auto [x, y] = coords;
1329 int new_tile_id = world_tiles[x][y];
1330 changes.push_back({x, y, old_tile_id, new_tile_id});
1331 }
1332 auto action = std::make_unique<OverworldTilePaintAction>(
1334 std::move(changes), &overworld_, [this]() { RefreshOverworldMap(); });
1335 undo_manager_.Push(std::move(action));
1336
1338}
1339
1341 const OverworldPropertyEdit& edit, bool record_undo) {
1343 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1344 }
1345
1346 if (!record_undo) {
1347 return map_properties_system_->ApplyPropertyEditDirect(edit);
1348 }
1349
1351 ASSIGN_OR_RETURN(const int before_value,
1352 map_properties_system_->ReadPropertyValue(edit));
1353
1354 OverworldPropertyEdit before = edit;
1355 before.value = before_value;
1356 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1357
1358 ASSIGN_OR_RETURN(const int after_value,
1359 map_properties_system_->ReadPropertyValue(edit));
1360 if (after_value == before_value) {
1361 return absl::OkStatus();
1362 }
1363
1364 OverworldPropertyEdit after = edit;
1365 after.value = after_value;
1366 undo_manager_.Push(std::make_unique<OverworldMapPropertyEditAction>(
1367 before, after,
1368 [this](const OverworldPropertyEdit& replay) {
1369 return this->ApplyOverworldPropertyEdit(replay, false);
1370 },
1372 return absl::OkStatus();
1373}
1374
1376 const std::vector<OverworldPropertyEdit>& edits,
1377 const std::string& description, bool record_undo) {
1379 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1380 }
1381 if (edits.empty()) {
1382 return absl::OkStatus();
1383 }
1384
1385 if (!record_undo) {
1386 for (const auto& edit : edits) {
1387 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1388 }
1389 return absl::OkStatus();
1390 }
1391
1393
1394 std::vector<OverworldPropertyEdit> before_edits;
1395 std::vector<OverworldPropertyEdit> after_edits;
1396 before_edits.reserve(edits.size());
1397 after_edits.reserve(edits.size());
1398
1399 for (const auto& edit : edits) {
1400 if (!map_properties_system_->CheckPropertyEditSupported(edit).ok()) {
1401 continue;
1402 }
1403
1404 auto before_value = map_properties_system_->ReadPropertyValue(edit);
1405 if (!before_value.ok()) {
1406 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1407 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1408 }
1409 return before_value.status();
1410 }
1411
1412 OverworldPropertyEdit before = edit;
1413 before.value = *before_value;
1414 const absl::Status apply_status =
1415 map_properties_system_->ApplyPropertyEditDirect(edit);
1416 if (!apply_status.ok()) {
1417 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1418 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1419 }
1420 return apply_status;
1421 }
1422
1423 auto after_value = map_properties_system_->ReadPropertyValue(edit);
1424 if (!after_value.ok()) {
1425 (void)map_properties_system_->ApplyPropertyEditDirect(before);
1426 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1427 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1428 }
1429 return after_value.status();
1430 }
1431 if (*after_value == *before_value) {
1432 continue;
1433 }
1434
1435 OverworldPropertyEdit after = edit;
1436 after.value = *after_value;
1437 before_edits.push_back(std::move(before));
1438 after_edits.push_back(std::move(after));
1439 }
1440
1441 if (after_edits.empty()) {
1442 return absl::OkStatus();
1443 }
1444
1445 const std::string action_description =
1446 description.empty()
1447 ? absl::StrFormat("Edit %d overworld map properties",
1448 static_cast<int>(after_edits.size()))
1449 : description;
1450 undo_manager_.Push(std::make_unique<OverworldMapPropertyBatchEditAction>(
1451 before_edits, after_edits,
1452 [this](const OverworldPropertyEdit& replay) {
1453 return this->ApplyOverworldPropertyEdit(replay, false);
1454 },
1455 action_description));
1456 return absl::OkStatus();
1457}
1458
1460 const std::string& type, int id, const std::string& label) {
1461 if (!dependencies_.project) {
1462 return absl::FailedPreconditionError("No project is open");
1463 }
1464
1466 const std::string before =
1470 const std::string after =
1472 if (after == before) {
1473 return absl::OkStatus();
1474 }
1475
1476 undo_manager_.Push(std::make_unique<OverworldProjectLabelEditAction>(
1477 before, after,
1478 [this, type, id](const std::string& value) {
1480 value);
1481 },
1482 absl::StrFormat("Rename %s 0x%02X", type, id)));
1483 return absl::OkStatus();
1484}
1485
1487 // Finalize any pending paint operation first
1489 return undo_manager_.Undo();
1490}
1491
1493 return undo_manager_.Redo();
1494}
1495
1496// ============================================================================
1497
1499 gfx::ScopedTimer timer("LoadGraphics");
1500
1501 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1502 // Load the Link to the Past overworld.
1503 {
1504 gfx::ScopedTimer load_timer("Overworld::Load");
1506 }
1508
1509 // Fix: Set transparency for the first color of each 16-color subpalette
1510 // This ensures the background color (backdrop) shows through
1511 for (size_t i = 0; i < palette_.size(); i += 16) {
1512 if (i < palette_.size()) {
1513 palette_[i].set_transparent(true);
1514 }
1515 }
1516
1517 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1518
1519 // Phase 1: Create bitmaps without textures for faster loading
1520 // This avoids blocking the main thread with GPU texture creation
1521 {
1522 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1528 }
1529
1530 LOG_DEBUG("OverworldEditor",
1531 "Loading overworld tileset (deferred textures).");
1532 {
1533 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1534 tile16_blockset_bmp_.Create(0x80, 0x2000, 0x08,
1539 }
1540 map_blockset_loaded_ = true;
1541
1542 // Copy the tile16 data into individual tiles.
1543 const auto& tile16_blockset_data = overworld_.tile16_blockset_data();
1544 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
1545
1546 {
1547 gfx::ScopedTimer tilemap_timer("CreateTilemap");
1549 gfx::CreateTilemap(renderer_, tile16_blockset_data, 0x80, 0x2000,
1551
1552 // Queue texture creation for the tile16 blockset atlas
1557 }
1558 }
1559
1560 // Phase 2: reset map bitmaps and leave them fully on-demand.
1561 // The canvas/navigation path will materialize only the visible/current maps.
1562 if (map_texture_) {
1563 map_texture_->ResetMapBitmaps();
1564 }
1565
1566 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
1567 {
1568 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
1570 }
1571 }
1572
1573 return absl::OkStatus();
1574}
1575
1577 // Render the sprites for each Overworld map
1578 const int depth = 0x10;
1579 for (int i = 0; i < 3; i++)
1580 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
1581 int width = sprite.width();
1582 int height = sprite.height();
1583 if (width == 0 || height == 0) {
1584 continue;
1585 }
1586 if (sprite_previews_.size() < sprite.id()) {
1587 sprite_previews_.resize(sprite.id() + 1);
1588 }
1589 sprite_previews_[sprite.id()].Create(width, height, depth,
1590 *sprite.preview_graphics());
1591 sprite_previews_[sprite.id()].SetPalette(palette_);
1594 &sprite_previews_[sprite.id()]);
1595 }
1596 return absl::OkStatus();
1597}
1598
1600 if (map_texture_)
1601 map_texture_->ProcessDeferredTextures();
1602}
1603
1605 if (map_texture_)
1606 map_texture_->EnsureMapTexture(map_index);
1607}
1608
1611 ctx.overworld = &overworld_;
1612 ctx.maps_bmp = &maps_bmp_;
1613 ctx.renderer = renderer_;
1616 ctx.refresh_map_on_demand = [this](int map_index) {
1617 this->RefreshOverworldMapOnDemand(map_index);
1618 };
1619 map_texture_ = std::make_unique<OverworldMapTextureCoordinator>(ctx);
1620}
1621
1624 ctx.rom = rom_;
1625 ctx.overworld = &overworld_;
1626 ctx.maps_bmp = &maps_bmp_;
1630 ctx.palette = &palette_;
1631 ctx.renderer = renderer_;
1636 ctx.game_state = &game_state_;
1638 ctx.status = &status_;
1639 ctx.ensure_map_texture = [this](int map_index) {
1640 this->EnsureMapTexture(map_index);
1641 };
1642 map_refresh_ = std::make_unique<MapRefreshCoordinator>(ctx);
1643}
1644
1646 if (canvas_nav_)
1647 canvas_nav_->HandleMapInteraction();
1648}
1649
1651 if (canvas_nav_)
1652 canvas_nav_->ScrollBlocksetCanvasToCurrentTile();
1653}
1654
1656 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1657 return;
1658 }
1659
1660 if (!rom_ || !rom_->is_loaded() || !all_gfx_loaded_ ||
1662 current_tile16_ = tile_id;
1664 return;
1665 }
1666
1668
1669 if (tile_id == tile16_editor_.current_tile16()) {
1670 current_tile16_ = tile_id;
1671 auto status = tile16_editor_.SetCurrentTile(tile_id);
1672 if (!status.ok()) {
1673 LOG_WARN("OverworldEditor", "Failed to refresh selected Tile16 %d: %s",
1674 tile_id, status.message().data());
1675 }
1676 return;
1677 }
1678
1679 const int from_tile = tile16_editor_.current_tile16();
1680 const bool had_staged_on_current = tile16_editor_.is_tile_modified(from_tile);
1681 if (had_staged_on_current && dependencies_.window_manager) {
1682 const size_t session_id =
1688 }
1689
1692}
1693
1696
1697 // Unregister palette listener
1698 if (palette_listener_id_ >= 0) {
1701 }
1702
1704 current_graphics_set_.clear();
1705 all_gfx_loaded_ = false;
1706 map_blockset_loaded_ = false;
1707 return absl::OkStatus();
1708}
1709
1711 if (canvas_nav_)
1712 canvas_nav_->UpdateBlocksetSelectorState();
1713}
1714
1716 const int next =
1717 std::clamp(current_tile16_ + delta, 0, zelda3::kNumTile16Individual - 1);
1719}
1720
1722 if (!status_bar)
1723 return;
1724 const char* world_label = "LW";
1725 switch (current_world_) {
1726 case 0:
1727 world_label = "LW";
1728 break;
1729 case 1:
1730 world_label = "DW";
1731 break;
1732 case 2:
1733 world_label = "SW";
1734 break;
1735 default:
1736 world_label = "??";
1737 break;
1738 }
1739 status_bar->SetCustomSegment(
1740 "Map", absl::StrFormat("%s #%02X", world_label, current_map_ & 0xFF));
1741 status_bar->SetCustomSegment("Tile16",
1742 absl::StrFormat("0x%03X", current_tile16_));
1743
1744 const char* mode_label = "Draw";
1745 switch (entity_edit_mode_) {
1747 mode_label = "Entrances";
1748 break;
1750 mode_label = "Exits";
1751 break;
1753 mode_label = "Items";
1754 break;
1756 mode_label = "Sprites";
1757 break;
1759 mode_label = "Transports";
1760 break;
1762 mode_label = "Music";
1763 break;
1765 switch (current_mode) {
1766 case EditingMode::MOUSE:
1767 mode_label = "Mouse";
1768 break;
1770 mode_label = "Draw";
1771 break;
1773 mode_label = "Fill";
1774 break;
1775 }
1776 break;
1777 }
1778 status_bar->SetEditorMode(mode_label);
1779}
1780
1781} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:146
bool is_loaded() const
Definition rom.h:144
static Flags & get()
Definition features.h:119
bool loaded() const
Check if the manifest has been loaded.
UndoManager undo_manager_
Definition editor.h:334
EditorDependencies dependencies_
Definition editor.h:333
EditorType type() const
Definition editor.h:306
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:54
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:36
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:441
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:448
static Arena & Get()
Definition arena.cc:21
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
bool is_active() const
Definition bitmap.h:405
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
SDL_Surface * surface() const
Definition bitmap.h:400
RAII timer for automatic timing management.
auto selected_tile_pos() const
Definition canvas.h:395
auto global_scale() const
Definition canvas.h:397
auto select_rect_active() const
Definition canvas.h:393
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:290
auto drawn_tile_position() const
Definition canvas.h:356
void ClearContextMenuItems()
Definition canvas.cc:863
const ImVector< ImVec2 > & points() const
Definition canvas.h:344
auto selected_points() const
Definition canvas.h:458
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: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
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
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
void CenterText(const char *text)
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