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
8#include "rom/transaction.h"
9#include "util/i18n/tr.h"
10#include "util/rom_hash.h"
11
12#ifndef IM_PI
13#define IM_PI 3.14159265358979323846f
14#endif
15
16// C system headers
17#include <cmath>
18#include <cstddef>
19#include <cstdint>
20
21// C++ standard library headers
22#include <algorithm>
23#include <exception>
24#include <filesystem>
25#include <iostream>
26#include <memory>
27#include <ostream>
28#include <set>
29#include <string>
30#include <unordered_map>
31#include <utility>
32#include <vector>
33
34// Third-party library headers
35#include "absl/status/status.h"
36#include "absl/strings/str_format.h"
37#include "imgui/imgui.h"
38
39// Project headers
53// Note: All overworld panels now self-register via REGISTER_PANEL macro:
54// AreaGraphicsPanel, DebugWindowPanel, GfxGroupsPanel, MapPropertiesPanel,
55// OverworldCanvasPanel, ScratchSpacePanel, Tile16EditorPanel, Tile16SelectorPanel,
56// Tile8SelectorPanel, UsageStatisticsPanel, V3SettingsPanel, OverworldItemListPanel
64#include "app/gfx/core/bitmap.h"
75#include "app/gui/core/icons.h"
77#include "app/gui/core/style.h"
82#include "core/asar_wrapper.h"
83#include "core/features.h"
85#include "core/project.h"
86#include "rom/rom.h"
87#include "util/file_util.h"
88#include "util/hex.h"
89#include "util/log.h"
90#include "util/macro.h"
91#include "zelda3/common.h"
99
100namespace yaze::editor {
101
102namespace {
103
105 const zelda3::OverworldItem& rhs) {
106 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
107 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.bg2_ == rhs.bg2_;
108}
109
111 const OverworldItemsSnapshot& rhs) {
112 if (lhs.items.size() != rhs.items.size()) {
113 return false;
114 }
115
116 for (size_t i = 0; i < lhs.items.size(); ++i) {
117 if (!ItemIdentityMatchesForUndo(lhs.items[i], rhs.items[i])) {
118 return false;
119 }
120 }
121
122 if (lhs.selected_item_identity.has_value() !=
123 rhs.selected_item_identity.has_value()) {
124 return false;
125 }
126 if (!lhs.selected_item_identity.has_value()) {
127 return true;
128 }
129
132}
133
134} // namespace
135
137 if (palette_listener_id_ >= 0) {
140 }
141
148
149 // These store callbacks and raw pointers into the editor. Release them while
150 // all editor-owned state is still alive rather than during member teardown.
151 sidebar_.reset();
153}
154
156 int& current_map) {
157 const int clamped_world = std::clamp(current_world, 0, 2);
158 int normalized_map = current_map;
159 if (normalized_map < 0 || normalized_map >= zelda3::kNumOverworldMaps) {
160 normalized_map =
161 std::min(clamped_world * 0x40, zelda3::kNumOverworldMaps - 1);
162 }
163
164 const int normalized_world = std::clamp(normalized_map / 0x40, 0, 2);
165 const bool changed =
166 normalized_world != current_world || normalized_map != current_map;
167 current_world = normalized_world;
168 current_map = normalized_map;
169 return changed;
170}
171
173 // Initialize renderer from dependencies
175
177 "Gfx Groups: selection syncs with the Graphics editor for this ROM "
178 "session "
179 "(this surface uses its own preview canvases).");
180
182
183 // Initialize MapRefreshCoordinator (must be before callbacks that use it)
185
186 if (rom_) {
187 upgrade_system_ = std::make_unique<zelda3::OverworldUpgradeSystem>(*rom_);
188 }
190 std::make_unique<EntityMutationService>(overworld_);
191
193
194 // Note: All WindowContent instances now self-register via REGISTER_PANEL macro
195 // and use ContentRegistry::Context to access the current editor.
196 // See comment at include section for the current set of view wrappers.
197
198 // Original initialization code below:
199 // Initialize MapPropertiesSystem with canvas and bitmap data
200 // Initialize cards
201 usage_stats_card_ = std::make_unique<UsageStatisticsCard>(&overworld_);
202 debug_window_card_ = std::make_unique<DebugWindowCard>();
203
204 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
206
207 // Set up refresh callbacks for MapPropertiesSystem
208 map_properties_system_->SetRefreshCallbacks(
209 [this](int map_id) { this->RefreshMapProperties(map_id); },
210 [this]() { this->RefreshOverworldMap(); },
211 [this](int map_id) -> absl::Status {
212 return this->RefreshMapPalette(map_id);
213 },
214 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
215 [this](int map_index) { this->ForceRefreshGraphics(map_index); });
216 map_properties_system_->SetMapSelectionCallback(
217 [this](int map_index, bool respect_pin) {
218 this->SelectMapForEditing(map_index, respect_pin);
219 });
220 map_properties_system_->SetPropertyEditCallback(
221 [this](const OverworldPropertyEdit& edit) {
222 return this->ApplyOverworldPropertyEdit(edit);
223 });
224 map_properties_system_->SetPropertyEditBatchCallback(
225 [this](const std::vector<OverworldPropertyEdit>& edits,
226 const std::string& description) {
227 return this->ApplyOverworldPropertyEdits(edits, description);
228 });
229 map_properties_system_->SetResourceLabelEditCallback(
230 [this](const std::string& type, int id, const std::string& label) {
231 return this->RenameProjectResourceLabelWithUndo(type, id, label);
232 });
233
234 // Initialize OverworldSidebar
235 sidebar_ = std::make_unique<OverworldSidebar>(&overworld_, rom_,
237
238 // Initialize OverworldEntityRenderer for entity visualization
239 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
241
242 sidebar_->SetRenameLabelCallback(
243 [this](const std::string& type, int id, const std::string& label) {
244 return this->RenameProjectResourceLabelWithUndo(type, id, label);
245 });
246
247 // Initialize canvas navigation and drawing tools. The sidebar also renders
248 // quick appearance fields above the canvas.
249 toolbar_ = std::make_unique<OverworldToolbar>();
250 toolbar_->on_story_changed = [this](int state) {
252 };
253 sidebar_->on_story_changed = toolbar_->on_story_changed;
254 toolbar_->on_world_changed = [this](int world) {
255 SwitchToWorld(world);
256 };
257 toolbar_->on_set_mode = [this](EditingMode mode) {
258 SetEditingMode(mode);
259 };
260 toolbar_->on_set_entity_mode = [this](EntityEditMode mode) {
261 SetEntityEditMode(mode);
262 };
263 toolbar_->on_insert_item = [this]() {
264 RequestItemInsertPopup(std::nullopt);
265 };
266 toolbar_->on_open_map_properties = [this]() {
268 };
269 toolbar_->on_toggle_overlay_preview = [this]() {
271 };
272 toolbar_->is_overlay_preview_enabled = [this]() {
274 };
275 toolbar_->on_toggle_grid = [this]() {
276 ToggleGrid();
277 };
278 toolbar_->is_grid_visible = [this]() {
279 return grid_visible();
280 };
281 toolbar_->on_toggle_entities = [this]() {
283 };
284 toolbar_->are_entities_visible = [this]() {
285 return entities_visible();
286 };
287 toolbar_->on_zoom_in = [this]() {
288 ZoomIn();
289 };
290 toolbar_->on_zoom_out = [this]() {
291 ZoomOut();
292 };
293 toolbar_->on_zoom_fit = [this]() {
294 ZoomToFit();
295 };
296 toolbar_->on_center_map = [this]() {
298 };
299 toolbar_->get_zoom = [this]() {
301 };
302 toolbar_->on_toggle_overlay_preview = [this]() {
304 };
305 toolbar_->is_overlay_preview_enabled = [this]() {
307 };
308 toolbar_->on_upgrade_rom_version = [this](int) {
309 ImGui::OpenPopup("UpgradeROMVersion");
310 };
311
312 // Initialize OverworldCanvasRenderer for canvas and panel drawing
313 canvas_renderer_ = std::make_unique<OverworldCanvasRenderer>(this);
314 map_properties_system_->SetOpenMapPropertiesCallback(
315 [this]() { OpenMapPropertiesWindow(); });
316 map_properties_system_->SetContextNavigationCallbacks(
317 [this]() { ResetOverworldView(); }, [this]() { ZoomIn(); },
318 [this]() { ZoomOut(); }, [this]() { ZoomToFit(); },
319 [this]() { CenterOverworldView(); });
320 map_properties_system_->SetContextViewToggles(
321 {[this]() { return grid_visible(); }, [this]() { ToggleGrid(); }},
322 {[this]() { return entities_visible(); },
323 [this]() { ToggleEntityVisibility(); }},
324 {[this]() { return show_overlay_preview_; },
326 map_properties_system_->SetCurrentMapProvider(
327 [this]() { return current_map_; });
328 map_properties_system_->SetMapJumpCallback(
329 [this](int map_id) { JumpToMap(map_id); });
330 map_properties_system_->SetShortcutHintProvider(
331 [this](const char* name) -> std::string {
334 : std::string();
335 });
336
340}
341
344 sink.on_set_editor_mode = [this](EditingMode mode) {
345 SetEditingMode(mode);
346 };
347 sink.on_set_entity_mode = [this](EntityEditMode mode) {
348 SetEntityEditMode(mode);
349 };
350 sink.on_pick_tile_from_hover = [this]() {
352 };
353 sink.on_duplicate_selected = [this]() {
354 (void)DuplicateSelectedItem();
355 };
356 sink.on_nudge_selected = [this](int delta_x, int delta_y, bool shift_held) {
357 const int step = shift_held ? 16 : 1;
358 (void)this->NudgeSelectedItem(delta_x * step, delta_y * step);
359 };
360 sink.on_entity_context_menu = [this](zelda3::GameEntity* entity) {
362 auto* workbench = static_cast<OverworldEntityWorkbench*>(
364 "overworld.entity_workbench"));
365 if (workbench) {
366 workbench->OpenContextMenuFor(entity);
367 }
368 }
369 };
370 sink.on_entity_double_click = [this](zelda3::GameEntity* entity) {
371 if (!entity) {
372 return;
373 }
374 switch (entity->entity_type_) {
376 this->jump_to_tab_ =
377 static_cast<zelda3::OverworldExit*>(entity)->room_id_;
378 break;
380 this->jump_to_tab_ =
381 static_cast<zelda3::OverworldEntrance*>(entity)->entrance_id_;
382 break;
383 default:
384 break;
385 }
386 };
387 sink.can_edit_items = [this]() {
388 // Items mode, or no entity focus with an item selected, in select mode.
390 return false;
391 }
393 return true;
394 }
396 GetSelectedItem() != nullptr;
397 };
398 sink.on_undo = [this]() {
399 status_ = Undo();
400 };
401 sink.on_redo = [this]() {
402 status_ = Redo();
403 };
405 std::make_unique<OverworldInteractionCoordinator>(std::move(sink));
406}
407
411 deps.overworld = &overworld_;
412 deps.maps_bmp = &maps_bmp_;
419 deps.rom = rom_;
421
422 TilePaintingCallbacks callbacks;
423 callbacks.create_undo_point = [this](int map_id, int world, int x, int y,
424 int old_tile_id) {
425 this->CreateUndoPoint(map_id, world, x, y, old_tile_id);
426 };
427 callbacks.finalize_paint_operation = [this]() {
429 };
430 callbacks.refresh_overworld_map_on_demand = [this](int map_index) {
431 this->RefreshOverworldMapOnDemand(map_index);
432 };
433 callbacks.scroll_blockset_to_current_tile = [this]() {
435 };
436 callbacks.request_tile16_selection = [this](int tile_id) {
437 this->RequestTile16Selection(tile_id);
438 };
439 callbacks.select_map_for_editing = [this](int map_id) {
440 if (map_id == current_map_) {
441 return;
442 }
443 // Respects the pin and refreshes the Tile16 selector palette for the
444 // newly selected map. A same-map stroke does not rebuild the atlas.
445 SelectMapForEditing(map_id, true);
446 };
447
448 tile_painting_ = std::make_unique<TilePaintingManager>(deps, callbacks);
449}
450
454 ctx.overworld = &overworld_;
455 ctx.rom = rom_;
464 ctx.maps_bmp = &maps_bmp_;
467
469 callbacks.refresh_overworld_map = [this]() {
470 this->RefreshOverworldMap();
471 };
472 callbacks.refresh_tile16_blockset = [this]() -> absl::Status {
473 return this->RefreshTile16Blockset();
474 };
475 callbacks.ensure_map_texture = [this](int map_index) {
476 this->EnsureMapTexture(map_index);
477 };
478 callbacks.select_map_for_editing = [this](int map_index, bool respect_pin) {
479 this->SelectMapForEditing(map_index, respect_pin);
480 };
481 callbacks.pick_tile16_from_hovered_canvas = [this]() -> bool {
482 return this->PickTile16FromHoveredCanvas();
483 };
484 callbacks.is_entity_hovered = [this]() -> bool {
485 return entity_renderer_ && entity_renderer_->hovered_entity() != nullptr;
486 };
487 callbacks.open_map_properties = [this]() {
489 };
490
491 canvas_nav_ = std::make_unique<CanvasNavigationManager>();
492 canvas_nav_->Initialize(ctx, callbacks);
493}
494
496 current_entity_ = entity;
497 if (!entity) {
499 return;
500 }
502 selected_item_identity_ = *static_cast<zelda3::OverworldItem*>(entity);
503 } else {
505 }
506}
507
513
515 const Rom& replacement) const {
516 if (!overworld_.is_loaded() ||
517 (!is_lyra_preview() && replacement.InspectLyraLayout().presence ==
519 (is_lyra_preview() &&
520 util::ComputeSha1Hex(replacement.data(), replacement.size()) ==
522 return absl::OkStatus();
523 }
524 return absl::FailedPreconditionError(
525 "Lyra canvas session replacement requires a fresh editor session");
526}
527
528absl::Status OverworldEditor::Load() {
529 gfx::ScopedTimer timer("OverworldEditor::Load");
530
531 LOG_DEBUG("OverworldEditor", "Loading overworld.");
532 if (!rom_ || !rom_->is_loaded()) {
533 return absl::FailedPreconditionError("ROM not loaded");
534 }
535 const auto lyra = rom_->InspectLyraLayout();
536 if (lyra.presence == rom::LyraLayoutPresence::kInvalid) {
537 return absl::FailedPreconditionError(
538 "Invalid Lyra descriptor; overworld loading refused");
539 }
541 lyra.presence == rom::LyraLayoutPresence::kValid &&
544 // The manager may call Load again after restoring the same ROM bytes.
545 // Keep the bound maps, descriptor pointer, textures and undo state.
546 return absl::OkStatus();
547 }
548 if (overworld_.is_loaded() &&
549 (lyra.presence == rom::LyraLayoutPresence::kValid ||
551 return absl::FailedPreconditionError(
552 "Lyra canvas session replacement requires a fresh editor session");
553 }
556
558 dependencies_.window_manager->RegisterPanelAlias("overworld.debug_window",
559 "overworld.debug");
560 }
561
562 // Clear undo/redo state when loading new ROM data
565
569 all_gfx_loaded_ = true;
570 return absl::OkStatus();
571 }
573 [this]() { FinalizePaintOperation(); });
577
578 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
582 [this](int id) { current_tile16_ = id; });
584 [this](int tile16_id, int quadrant) {
585 return OpenGraphicsForTile16Quadrant(tile16_id, quadrant);
586 });
587
589 [this](const std::vector<Tile16Commit>&) {
590 if (map_refresh_) {
591 map_refresh_->InvalidateTile16Definitions();
594 }
595 });
596
597 // Set up entity insertion callback for MapPropertiesSystem
599 map_properties_system_->SetEntityCallbacks(
600 [this](const std::string& entity_type,
601 const OverworldContextTarget& target) {
602 HandleEntityInsertion(entity_type, target);
603 });
604
605 // Set up tile16 edit callback for context menu in MOUSE mode
606 map_properties_system_->SetTile16SampleCallback(
607 [this](const OverworldContextTarget& target) {
608 return SampleContextTile16(target);
609 });
610 map_properties_system_->SetTile16EditCallback(
611 [this](const OverworldContextTarget& target) {
612 HandleTile16Edit(target);
613 });
614 map_properties_system_->SetTile16GraphicsCallback(
615 [this](const OverworldContextTarget& target) {
617 });
618 }
619
621
622 // Register as palette listener to refresh graphics when palettes change
623 if (palette_listener_id_ < 0) {
625 [this](const std::string& group_name, int palette_index) {
626 if (!gfx::PaletteManager::Get().IsManaging(game_data_) ||
628 return;
629 }
630 status_ =
631 map_refresh_->RefreshPaletteUsers(group_name, palette_index);
632 });
633 LOG_DEBUG("OverworldEditor", "Registered as palette listener (ID: %d)",
635 }
636
637 all_gfx_loaded_ = true;
638
639 auto scratch_status = LoadScratchPad();
640 if (!scratch_status.ok()) {
641 LOG_WARN("OverworldEditor", "Failed to load scratch pad: %s",
642 std::string(scratch_status.message()).c_str());
643 }
644
646 const int pending = *pending_tile16_selection_after_gfx_;
648 RequestTile16Selection(pending);
649 }
650
651 return absl::OkStatus();
652}
653
655 state = std::clamp(state, 0, 2);
656 if (state == game_state_)
657 return;
660 SetCurrentEntity(nullptr);
661 }
662 game_state_ = state;
664 for (int id = 0; id < zelda3::kNumOverworldMaps; ++id) {
665 if (auto* map = overworld_.mutable_overworld_map(id)) {
666 map->set_game_state(state);
668 }
669 }
670 if (all_gfx_loaded_) {
673 }
674}
675
678 bool refresh_selection = false;
679 for (int map = 0; map < zelda3::kNumOverworldMaps; ++map) {
680 auto* overworld_map = overworld_.mutable_overworld_map(map);
681 if (overworld_map != nullptr) {
682 overworld_map->set_game_state(std::clamp(game_state_, 0, 2));
683 }
684 // A map already marked for refresh waits for its rebuild, which records
685 // the new sheet revisions; marking it again every frame would keep
686 // clearing the blockset cache.
687 if (overworld_map != nullptr && !maps_bmp_[map].modified() &&
688 overworld_map->SourceSheetsChanged()) {
690 refresh_selection |= map == current_map_;
691 }
692 }
693 if (refresh_selection) {
694 // Keep the selected map, Tile16 thumbnails and pixel-source metadata in
695 // agreement even when the map canvas is closed. Other maps refresh lazily.
698 }
699}
700
702 status_ = absl::OkStatus();
703
704 // Safety check: Ensure ROM is loaded and graphics are ready
705 if (!rom_ || !rom_->is_loaded()) {
707 return absl::OkStatus();
708 }
709
710 if (!all_gfx_loaded_) {
711 gui::DrawEmptyState(gui::EmptyLoading("Loading overworld graphics…"));
712 return absl::OkStatus();
713 }
714
718 return status_;
719 }
720
723
724 // Process deferred textures for smooth loading
726
727 // Show unsaved shared assignments and pixels in maps using those sources.
729
730 // Early return if window_manager is not available
731 // (panels won't be drawn without it, so no point continuing)
733 return status_;
734 }
735
737 return status_;
738 }
739
740 // ===========================================================================
741 // Main Overworld Canvas
742 // ===========================================================================
743 // The panels (Tile16 Selector, Area Graphics, etc.) are now managed by
744 // WindowContent/WorkspaceWindowManager and drawn automatically. This section only
745 // handles the main canvas and toolbar.
746
747 // ===========================================================================
748 // Non-Panel Windows (not managed by WindowContent system)
749 // ===========================================================================
750 // These are separate feature windows, not part of the panel system
751
752 // Custom Background Color Editor
754 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
755 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color",
757 if (rom_->is_loaded() && overworld_.is_loaded() &&
759 map_properties_system_->DrawCustomBackgroundColorEditor(
761 }
762 }
763 ImGui::End();
764 }
765
766 // Visual Effects Editor (Subscreen Overlays)
768 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
769 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor",
771 if (rom_->is_loaded() && overworld_.is_loaded() &&
773 map_properties_system_->DrawOverlayEditor(current_map_,
775 }
776 }
777 ImGui::End();
778 }
779
780 // Note: Tile16 Editor is now managed as an WindowContent (Tile16EditorPanel)
781 // It uses UpdateAsPanel() which provides a context menu instead of MenuBar
782
783 if (auto* workbench = GetWorkbench()) {
784 workbench->ProcessPendingInsertion(entity_mutation_service_.get());
785 workbench->DrawPopups();
786 }
787
788 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr,
789 ImGuiWindowFlags_AlwaysAutoResize)) {
790 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
791 ImGui::Separator();
792 ImGui::TextWrapped(
793 tr("This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
794 "enabling advanced overworld features."));
795 ImGui::Separator();
796
797 const uint8_t current_version =
799 : 0xFF;
800 ImGui::Text(tr("Current Version: %s"),
801 current_version == 0xFF
802 ? "Vanilla"
803 : absl::StrFormat("v%d", current_version).c_str());
804
805 static int target_version = 3;
806 ImGui::RadioButton(tr("v2 (Basic features)"), &target_version, 2);
807 ImGui::SameLine();
808 ImGui::RadioButton(tr("v3 (All features)"), &target_version, 3);
809
810 ImGui::Separator();
811
812 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150.0f, 0.0f))) {
813 auto upgrade_status =
814 upgrade_system_ != nullptr
815 ? upgrade_system_->ApplyZSCustomOverworldASM(target_version)
816 : absl::FailedPreconditionError("Upgrade system not initialized");
817 if (upgrade_status.ok()) {
818 status_ = Clear();
819 if (status_.ok()) {
820 status_ = Load();
821 }
822 if (status_.ok()) {
823 ImGui::CloseCurrentPopup();
824 }
825 } else {
826 LOG_ERROR("OverworldEditor", "Upgrade failed: %s",
827 upgrade_status.message().data());
828 }
829 }
830 ImGui::SameLine();
831 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150.0f, 0.0f))) {
832 ImGui::CloseCurrentPopup();
833 }
834
835 ImGui::EndPopup();
836 }
837
838 // ===========================================================================
839 // Centralized Entity Interaction Logic (extracted to dedicated method)
840 // ===========================================================================
841 zelda3::GameEntity* hovered_entity =
842 entity_renderer_ ? entity_renderer_->hovered_entity() : nullptr;
844 interaction_coordinator_->Update(hovered_entity);
845 }
846
847 return absl::OkStatus();
848}
849
852
853 if (canvas_renderer_) {
854 canvas_renderer_->DrawOverworldCanvas();
855 }
856}
857
859 bool changed = false;
861 const int normalized =
862 std::clamp(current_map_, 0, overworld_.inspected_map_count() - 1);
863 const int world = normalized / 0x40;
864 changed = normalized != current_map_ || world != current_world_;
865 current_map_ = normalized;
866 current_world_ = world;
867 } else {
869 }
870
873 if (const auto* map = overworld_.overworld_map(current_map_)) {
874 current_parent_ = map->parent();
875 } else {
877 }
878
879 if (changed) {
880 LOG_WARN("OverworldEditor",
881 "Normalized stale overworld selection to world=%d map=%d",
883 }
884 return changed;
885}
886
887void OverworldEditor::SelectMapForEditing(int map_id, bool respect_pin) {
888 if (respect_pin && current_map_lock_) {
889 return;
890 }
891 if (map_id < 0 || map_id >= overworld_.inspected_map_count()) {
892 return;
893 }
894 const auto* map = overworld_.overworld_map(map_id);
895 if (!map) {
896 return;
897 }
899 current_map_ = map_id;
900 current_world_ = map_id / 0x40;
901 current_parent_ = map->parent();
905 if (status_.ok()) {
906 EnsureMapTexture(map_id);
907 }
908 return;
909 }
910
911 // Cursor tracking may cross screens during one paint stroke. Preserve its
912 // undo batch within the same world; explicit selection still finalizes it.
913 const bool tracking_paint = respect_pin &&
916 map_id / 0x40 == current_world_;
917 if (!tracking_paint) {
919 }
920 current_map_ = map_id;
921 current_world_ = std::clamp(map_id / 0x40, 0, 2);
922 current_parent_ = map->parent();
925
926 if (all_gfx_loaded_) {
928 }
930 if (!status_.ok()) {
932 return;
933 }
934
936 if (all_gfx_loaded_) {
939 }
940}
941
942void OverworldEditor::PrimeWorldMaps(int world, bool process_texture_queue) {
943 if (map_texture_)
944 map_texture_->PrimeWorldMaps(world, process_texture_queue);
945}
946
949
950 const int clamped_world = std::clamp(world, 0, 2);
951 const int local_map = current_map_ & 0x3F;
952 const int world_start = clamped_world * 0x40;
953 const int maps_remaining = overworld_.inspected_map_count() - world_start;
954 if (maps_remaining <= 0) {
955 return;
956 }
957
958 hovered_map_ = -1;
959 SelectMapForEditing(world_start + std::min(local_map, maps_remaining - 1),
960 false);
964 return;
965 }
966
968 if (!status_.ok()) {
970 return;
971 }
972
978}
979
981 const zelda3::OverworldItem& item_identity) {
982 auto* item = FindItemByIdentity(&overworld_, item_identity);
983 if (!item) {
984 return false;
985 }
986
988 current_entity_ = item;
990 auto* workbench = static_cast<OverworldEntityWorkbench*>(
992 "overworld.entity_workbench"));
993 if (workbench) {
994 workbench->SetActiveEntity(item);
995 }
996 }
997 return true;
998}
999
1002 current_entity_ = nullptr;
1004 auto* workbench = static_cast<OverworldEntityWorkbench*>(
1006 "overworld.entity_workbench"));
1007 if (workbench) {
1008 workbench->SetActiveEntity(nullptr);
1009 }
1010 }
1011}
1012
1014 if (is_lyra_preview()) {
1015 return nullptr;
1016 }
1018 return nullptr;
1019 return static_cast<OverworldEntityWorkbench*>(
1021 "overworld.entity_workbench"));
1022}
1023
1025 if (!selected_item_identity_.has_value()) {
1026 return nullptr;
1027 }
1028
1030 if (!item) {
1032 return nullptr;
1033 }
1034
1035 current_entity_ = item;
1036 if (auto* workbench = GetWorkbench()) {
1037 workbench->SetActiveEntity(item);
1038 }
1039 return item;
1040}
1041
1043 return const_cast<OverworldEditor*>(this)->GetSelectedItem();
1044}
1045
1052
1054 const OverworldItemsSnapshot& snapshot) {
1055 auto* items = overworld_.mutable_all_items();
1056 if (!items) {
1057 return;
1058 }
1059
1060 *items = snapshot.items;
1062 if (selected_item_identity_.has_value()) {
1063 auto* selected_item =
1065 if (selected_item) {
1066 current_entity_ = selected_item;
1067 if (auto* workbench = GetWorkbench()) {
1068 workbench->SetActiveEntity(selected_item);
1069 }
1070 } else {
1072 }
1073 } else {
1075 }
1076
1077 if (rom_) {
1078 rom_->set_dirty(true);
1079 }
1081}
1082
1084 std::string description) {
1086 if (ItemSnapshotsEqual(before, after)) {
1087 return;
1088 }
1089
1090 undo_manager_.Push(std::make_unique<OverworldItemsEditAction>(
1091 std::move(before), std::move(after),
1092 [this](const OverworldItemsSnapshot& snapshot) {
1093 RestoreItemUndoSnapshot(snapshot);
1094 },
1095 std::move(description)));
1096}
1097
1098bool OverworldEditor::DuplicateSelectedItem(int offset_x, int offset_y) {
1099 auto* selected_item = GetSelectedItem();
1100 if (!selected_item) {
1103 "Select an overworld item first (click one, or Items mode: 5)",
1105 }
1106 return false;
1107 }
1108
1109 auto before_snapshot = CaptureItemUndoSnapshot();
1112 duplicate_result = entity_mutation_service_->DuplicateItem(
1113 *selected_item, offset_x, offset_y);
1114 } else {
1115 auto duplicate_or = DuplicateItemByIdentity(&overworld_, *selected_item,
1116 offset_x, offset_y);
1117 if (duplicate_or.ok()) {
1118 duplicate_result.entity = *duplicate_or;
1119 duplicate_result.status = absl::OkStatus();
1120 } else {
1121 duplicate_result.status = duplicate_or.status();
1122 duplicate_result.error_message =
1123 "Failed to duplicate overworld item: " +
1124 std::string(duplicate_or.status().message());
1125 }
1126 }
1127 if (!duplicate_result.ok()) {
1130 duplicate_result.error_message.empty()
1131 ? "Failed to duplicate overworld item"
1132 : duplicate_result.error_message,
1134 }
1135 return false;
1136 }
1137
1138 auto* duplicated_item =
1139 static_cast<zelda3::OverworldItem*>(duplicate_result.entity);
1140 selected_item_identity_ = *duplicated_item;
1141 current_entity_ = duplicated_item;
1142 if (auto* workbench = GetWorkbench()) {
1143 workbench->SetActiveEntity(duplicated_item);
1144 }
1145 PushItemUndoAction(std::move(before_snapshot),
1146 absl::StrFormat("Duplicate overworld item 0x%02X",
1147 static_cast<int>(duplicated_item->id_)));
1148 rom_->set_dirty(true);
1151 absl::StrFormat("Duplicated item 0x%02X",
1152 static_cast<int>(duplicated_item->id_)),
1154 }
1155 return true;
1156}
1157
1158bool OverworldEditor::NudgeSelectedItem(int delta_x, int delta_y) {
1159 auto* selected_item = GetSelectedItem();
1160 if (!selected_item) {
1161 return false;
1162 }
1163
1164 auto before_snapshot = CaptureItemUndoSnapshot();
1165 auto status = NudgeItem(selected_item, delta_x, delta_y);
1166 if (!status.ok()) {
1168 dependencies_.toast_manager->Show("Failed to move selected item",
1170 }
1171 return false;
1172 }
1173
1174 selected_item_identity_ = *selected_item;
1175 current_entity_ = selected_item;
1176 if (auto* workbench = GetWorkbench()) {
1177 workbench->SetActiveEntity(selected_item);
1178 }
1180 std::move(before_snapshot),
1181 absl::StrFormat("Move overworld item 0x%02X (%+d,%+d)",
1182 static_cast<int>(selected_item->id_), delta_x, delta_y));
1183 rom_->set_dirty(true);
1184 return true;
1185}
1186
1188 auto* selected_item = GetSelectedItem();
1189 if (!selected_item) {
1190 return false;
1191 }
1192
1193 auto before_snapshot = CaptureItemUndoSnapshot();
1194 const zelda3::OverworldItem selected_identity = *selected_item;
1195 const uint8_t deleted_item_id = selected_identity.id_;
1196 absl::Status remove_status =
1197 absl::FailedPreconditionError("Entity mutation service not initialized");
1199 remove_status =
1200 entity_mutation_service_->DeleteItem(selected_identity).status;
1201 } else {
1202 remove_status = RemoveItemByIdentity(&overworld_, selected_identity);
1203 }
1204 if (!remove_status.ok()) {
1206 dependencies_.toast_manager->Show("Failed to delete selected item",
1208 }
1209 return false;
1210 }
1211
1212 auto* nearest_item =
1214 ? entity_mutation_service_->ResolveNextSelection(selected_identity)
1215 : FindNearestItemForSelection(&overworld_, selected_identity);
1216 if (nearest_item) {
1217 selected_item_identity_ = *nearest_item;
1218 current_entity_ = nearest_item;
1219 if (auto* workbench = GetWorkbench()) {
1220 workbench->SetActiveEntity(nearest_item);
1221 }
1222 } else {
1224 }
1225
1226 PushItemUndoAction(std::move(before_snapshot),
1227 absl::StrFormat("Delete overworld item 0x%02X",
1228 static_cast<int>(deleted_item_id)));
1229 rom_->set_dirty(true);
1231 if (nearest_item) {
1233 absl::StrFormat("Deleted item 0x%02X (selected nearest 0x%02X)",
1234 static_cast<int>(deleted_item_id),
1235 static_cast<int>(nearest_item->id_)),
1237 } else {
1239 absl::StrFormat("Deleted overworld item 0x%02X",
1240 static_cast<int>(deleted_item_id)),
1242 }
1243 }
1244 return true;
1245}
1246
1248 tile_painting_->CheckForOverworldEdits();
1249}
1250
1253 return absl::FailedPreconditionError("Clipboard unavailable");
1254 }
1255 if (tile_painting_) {
1256 if (const auto* brush = tile_painting_->selection_brush()) {
1261 return absl::OkStatus();
1262 }
1263 }
1264 // Single tile copy fallback
1265 if (current_tile16_ >= 0) {
1270 return absl::OkStatus();
1271 }
1272 return absl::FailedPreconditionError("Nothing selected to copy");
1273}
1274
1276 if (is_lyra_preview()) {
1277 return absl::FailedPreconditionError("Lyra preview is read-only");
1278 }
1280 return absl::FailedPreconditionError("Clipboard unavailable");
1281 }
1283 return absl::FailedPreconditionError("Clipboard empty");
1284 }
1285 if (!tile_painting_) {
1286 return absl::FailedPreconditionError("Tile painting is unavailable");
1287 }
1288 const auto& clipboard = *dependencies_.shared_clipboard;
1289 return tile_painting_->PasteBrush({clipboard.overworld_width,
1290 clipboard.overworld_height,
1291 clipboard.overworld_tile16_ids});
1292}
1293
1295 if (canvas_nav_)
1296 return canvas_nav_->CheckForCurrentMap();
1297 return absl::OkStatus();
1298}
1299
1301 if (is_lyra_preview()) {
1302 ImGui::TextDisabled("Lyra preview: graphics editing unavailable");
1303 return absl::OkStatus();
1304 }
1305 // Delegate to the existing GfxGroupEditor
1306 if (rom_ && rom_->is_loaded()) {
1307 return gfx_group_editor_.Update();
1308 } else {
1309 gui::DrawEmptyState(gui::EmptyNoRom(/*compact=*/true));
1310 return absl::OkStatus();
1311 }
1312}
1313
1314// DrawV3Settings - now in OverworldCanvasRenderer
1315
1316// DrawMapProperties - now in OverworldCanvasRenderer
1317
1320 return absl::FailedPreconditionError(
1321 "Overworld save transaction is already active");
1322 }
1325 return absl::OkStatus();
1326}
1327
1335
1343
1345 if (rom_ == nullptr) {
1346 return absl::FailedPreconditionError("No ROM for overworld sprites");
1347 }
1349 // Validate sprite capacity before any editor serializer changes ROM bytes.
1350 ASSIGN_OR_RETURN(auto sprite_plan, overworld_.PrepareSpriteSave());
1351 if (!sprite_plan.empty() && dependencies_.project &&
1353 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1354 for (const auto& write : sprite_plan) {
1355 ranges.emplace_back(write.address, write.address + write.bytes.size());
1356 }
1360 "overworld sprites", "OverworldEditor", dependencies_.toast_manager));
1361 }
1362
1363 ScopedRomTransaction save_transaction(*rom_);
1364 // HACK MANIFEST VALIDATION
1365 const bool saving_maps =
1367 if (saving_maps && dependencies_.project &&
1369 const auto& manifest = dependencies_.project->hack_manifest;
1370 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1371
1372 // Calculate memory ranges that would be written by overworld map saves.
1373 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1374 // address space (LoROM), so convert before analysis.
1375 auto ranges = overworld_.GetProjectedWriteRanges();
1377 manifest, write_policy, ranges, "overworld maps", "OverworldEditor",
1379 }
1380
1381 if (saving_maps) {
1382 const auto profile = zelda3::DetectOverworldRomProfile(*rom_);
1384 if (profile.has_expanded_tile32) {
1386 } else {
1388 }
1389 if (profile.has_expanded_tile16) {
1391 } else {
1393 }
1395 }
1396 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1398 }
1399 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1401 }
1402 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1404 }
1405 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1409 }
1411 save_transaction.Commit();
1412 return absl::OkStatus();
1413}
1414
1415// ============================================================================
1416// Undo/Redo System Implementation
1417// ============================================================================
1418
1420 switch (world) {
1421 case 0:
1422 return overworld_.mutable_map_tiles()->light_world;
1423 case 1:
1424 return overworld_.mutable_map_tiles()->dark_world;
1425 default:
1426 return overworld_.mutable_map_tiles()->special_world;
1427 }
1428}
1429
1430void OverworldEditor::CreateUndoPoint(int map_id, int world, int x, int y,
1431 int old_tile_id) {
1432 auto now = std::chrono::steady_clock::now();
1433
1434 // A stroke can cross screen boundaries. Keep one batch within the same world
1435 // until release; the undo action refreshes every screen touched by its tiles.
1436 if (current_paint_operation_.has_value() &&
1437 current_paint_operation_->world == world) {
1438 // Add to existing operation
1439 current_paint_operation_->tile_changes.emplace_back(std::make_pair(x, y),
1440 old_tile_id);
1441 } else {
1442 // Finalize any pending operation before starting a new one
1444
1445 // Start new operation
1447 OverworldUndoPoint{.map_id = map_id,
1448 .world = world,
1449 .tile_changes = {{{x, y}, old_tile_id}},
1450 .timestamp = now};
1451 }
1452}
1453
1455 if (!current_paint_operation_.has_value()) {
1456 return;
1457 }
1458
1459 // Push to the UndoManager (new framework path).
1460 auto& world_tiles = GetWorldTiles(current_paint_operation_->world);
1461 std::vector<OverworldTileChange> changes;
1462 changes.reserve(current_paint_operation_->tile_changes.size());
1463 for (const auto& [coords, old_tile_id] :
1464 current_paint_operation_->tile_changes) {
1465 auto [x, y] = coords;
1466 int new_tile_id = world_tiles[x][y];
1467 changes.push_back({x, y, old_tile_id, new_tile_id});
1468 }
1469 auto action = std::make_unique<OverworldTilePaintAction>(
1471 std::move(changes), &overworld_, std::function<void()>{},
1472 [this](int map_id) {
1473 maps_bmp_[map_id].set_modified(true);
1474 RefreshOverworldMapOnDemand(map_id);
1475 },
1476 /*allow_merge=*/false);
1477 undo_manager_.Push(std::move(action));
1478
1480}
1481
1483 const OverworldPropertyEdit& edit, bool record_undo) {
1484 if (is_lyra_preview()) {
1485 return absl::FailedPreconditionError("Lyra preview is read-only");
1486 }
1488 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1489 }
1490
1491 if (!record_undo) {
1492 return map_properties_system_->ApplyPropertyEditDirect(edit);
1493 }
1494
1496 ASSIGN_OR_RETURN(const int before_value,
1497 map_properties_system_->ReadPropertyValue(edit));
1498
1499 OverworldPropertyEdit before = edit;
1500 before.value = before_value;
1501 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1502
1503 ASSIGN_OR_RETURN(const int after_value,
1504 map_properties_system_->ReadPropertyValue(edit));
1505 if (after_value == before_value) {
1506 return absl::OkStatus();
1507 }
1508
1509 OverworldPropertyEdit after = edit;
1510 after.value = after_value;
1511 undo_manager_.Push(std::make_unique<OverworldMapPropertyEditAction>(
1512 before, after,
1513 [this](const OverworldPropertyEdit& replay) {
1514 return this->ApplyOverworldPropertyEdit(replay, false);
1515 },
1517 return absl::OkStatus();
1518}
1519
1521 const std::vector<OverworldPropertyEdit>& edits,
1522 const std::string& description, bool record_undo) {
1523 if (is_lyra_preview()) {
1524 return absl::FailedPreconditionError("Lyra preview is read-only");
1525 }
1527 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1528 }
1529 if (edits.empty()) {
1530 return absl::OkStatus();
1531 }
1532
1533 if (!record_undo) {
1534 for (const auto& edit : edits) {
1535 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1536 }
1537 return absl::OkStatus();
1538 }
1539
1541
1542 std::vector<OverworldPropertyEdit> before_edits;
1543 std::vector<OverworldPropertyEdit> after_edits;
1544 before_edits.reserve(edits.size());
1545 after_edits.reserve(edits.size());
1546
1547 for (const auto& edit : edits) {
1548 if (!map_properties_system_->CheckPropertyEditSupported(edit).ok()) {
1549 continue;
1550 }
1551
1552 auto before_value = map_properties_system_->ReadPropertyValue(edit);
1553 if (!before_value.ok()) {
1554 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1555 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1556 }
1557 return before_value.status();
1558 }
1559
1560 OverworldPropertyEdit before = edit;
1561 before.value = *before_value;
1562 const absl::Status apply_status =
1563 map_properties_system_->ApplyPropertyEditDirect(edit);
1564 if (!apply_status.ok()) {
1565 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1566 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1567 }
1568 return apply_status;
1569 }
1570
1571 auto after_value = map_properties_system_->ReadPropertyValue(edit);
1572 if (!after_value.ok()) {
1573 (void)map_properties_system_->ApplyPropertyEditDirect(before);
1574 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1575 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1576 }
1577 return after_value.status();
1578 }
1579 if (*after_value == *before_value) {
1580 continue;
1581 }
1582
1583 OverworldPropertyEdit after = edit;
1584 after.value = *after_value;
1585 before_edits.push_back(std::move(before));
1586 after_edits.push_back(std::move(after));
1587 }
1588
1589 if (after_edits.empty()) {
1590 return absl::OkStatus();
1591 }
1592
1593 const std::string action_description =
1594 description.empty()
1595 ? absl::StrFormat("Edit %d overworld map properties",
1596 static_cast<int>(after_edits.size()))
1597 : description;
1598 undo_manager_.Push(std::make_unique<OverworldMapPropertyBatchEditAction>(
1599 before_edits, after_edits,
1600 [this](const OverworldPropertyEdit& replay) {
1601 return this->ApplyOverworldPropertyEdit(replay, false);
1602 },
1603 action_description));
1604 return absl::OkStatus();
1605}
1606
1608 const std::string& type, int id, const std::string& label) {
1609 if (is_lyra_preview()) {
1610 return absl::FailedPreconditionError("Lyra preview is read-only");
1611 }
1612 if (!dependencies_.project) {
1613 return absl::FailedPreconditionError("No project is open");
1614 }
1615
1617 const std::string before =
1621 const std::string after =
1623 if (after == before) {
1624 return absl::OkStatus();
1625 }
1626
1627 undo_manager_.Push(std::make_unique<OverworldProjectLabelEditAction>(
1628 before, after,
1629 [this, type, id](const std::string& value) {
1631 value);
1632 },
1633 absl::StrFormat("Rename %s 0x%02X", type, id)));
1634 return absl::OkStatus();
1635}
1636
1638 if (is_lyra_preview()) {
1639 return absl::FailedPreconditionError("Lyra preview is read-only");
1640 }
1641 // Finalize any pending paint operation first
1643 return undo_manager_.Undo();
1644}
1645
1647 if (is_lyra_preview()) {
1648 return absl::FailedPreconditionError("Lyra preview is read-only");
1649 }
1651 return undo_manager_.Redo();
1652}
1653
1654// ============================================================================
1655
1657 const auto* owner = dependencies_.project;
1658 const std::string path =
1659 owner && !owner->overworld_graphics_preview.empty()
1660 ? owner->GetAbsolutePath(owner->overworld_graphics_preview)
1661 : std::string{};
1662 if (!force && path == graphics_preview_config_path_)
1663 return absl::OkStatus();
1665 // An invalid new config must not leave the previous project's art active.
1667 auto preview = core::LoadOverworldGraphicsPreview(path);
1668 if (preview.ok())
1669 overworld_.SetGraphicsPreview(std::move(*preview));
1670 if (!force && overworld_.is_loaded()) {
1671 for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
1672 maps_bmp_[i].set_modified(true);
1673 }
1675 }
1676 return preview.status();
1677}
1678
1680 gfx::ScopedTimer timer("LoadGraphics");
1681
1682 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1684 // The model can inspect legacy overlays, but the read-only canvas does not
1685 // yet qualify their final compositing for this descriptor format.
1686 show_overlay_preview_ = false;
1689 current_map_ = 0xA0;
1690 current_world_ = 2;
1696 if (map_texture_) {
1697 map_texture_->ResetMapBitmaps();
1698 }
1699 return absl::OkStatus();
1700 }
1702 // Load the Link to the Past overworld.
1703 {
1704 gfx::ScopedTimer load_timer("Overworld::Load");
1707 }
1709
1710 // Fix: Set transparency for the first color of each 16-color subpalette
1711 // This ensures the background color (backdrop) shows through
1712 for (size_t i = 0; i < palette_.size(); i += 16) {
1713 if (i < palette_.size()) {
1714 palette_[i].set_transparent(true);
1715 }
1716 }
1717
1718 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1719
1720 // Phase 1: Create bitmaps without textures for faster loading
1721 // This avoids blocking the main thread with GPU texture creation
1722 {
1723 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1729 }
1730
1731 LOG_DEBUG("OverworldEditor",
1732 "Loading overworld tileset (deferred textures).");
1733 {
1734 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1735 tile16_blockset_bmp_.Create(0x80, 0x2000, 0x08,
1740 }
1741 map_blockset_loaded_ = true;
1742
1743 // Copy the tile16 data into individual tiles.
1744 const auto& tile16_blockset_data = overworld_.tile16_blockset_data();
1745 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
1746
1747 {
1748 gfx::ScopedTimer tilemap_timer("CreateTilemap");
1750 gfx::CreateTilemap(renderer_, tile16_blockset_data, 0x80, 0x2000,
1752
1753 // Queue texture creation for the tile16 blockset atlas
1758 }
1759 }
1760
1761 // Phase 2: reset map bitmaps and leave them fully on-demand.
1762 // The canvas/navigation path will materialize only the visible/current maps.
1763 if (map_texture_) {
1764 map_texture_->ResetMapBitmaps();
1765 }
1766
1767 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
1768 {
1769 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
1771 }
1772 }
1773
1774 return absl::OkStatus();
1775}
1776
1778 // Render the sprites for each Overworld map
1779 const int depth = 0x10;
1780 for (int i = 0; i < 3; i++)
1781 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
1782 int width = sprite.width();
1783 int height = sprite.height();
1784 if (width == 0 || height == 0) {
1785 continue;
1786 }
1787 if (sprite_previews_.size() < sprite.id()) {
1788 sprite_previews_.resize(sprite.id() + 1);
1789 }
1790 sprite_previews_[sprite.id()].Create(width, height, depth,
1791 *sprite.preview_graphics());
1792 sprite_previews_[sprite.id()].SetPalette(palette_);
1795 &sprite_previews_[sprite.id()]);
1796 }
1797 return absl::OkStatus();
1798}
1799
1801 if (map_texture_)
1802 map_texture_->ProcessDeferredTextures();
1803}
1804
1806 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
1807 return nullptr;
1808 }
1809 EnsureMapTexture(map_id);
1810 const gfx::Bitmap& bitmap = maps_bmp_[map_id];
1811 return bitmap.is_active() && bitmap.texture() ? &bitmap : nullptr;
1812}
1813
1815 if (map_texture_)
1816 map_texture_->EnsureMapTexture(map_index);
1817}
1818
1821 ctx.overworld = &overworld_;
1822 ctx.maps_bmp = &maps_bmp_;
1823 ctx.renderer = renderer_;
1826 ctx.refresh_map_on_demand = [this](int map_index) {
1827 this->RefreshOverworldMapOnDemand(map_index);
1828 };
1829 map_texture_ = std::make_unique<OverworldMapTextureCoordinator>(ctx);
1830}
1831
1834 ctx.rom = rom_;
1835 ctx.overworld = &overworld_;
1836 ctx.maps_bmp = &maps_bmp_;
1840 ctx.palette = &palette_;
1841 ctx.renderer = renderer_;
1846 ctx.game_state = &game_state_;
1848 ctx.status = &status_;
1849 ctx.ensure_map_texture = [this](int map_index) {
1850 this->EnsureMapTexture(map_index);
1851 };
1852 map_refresh_ = std::make_unique<MapRefreshCoordinator>(ctx);
1853}
1854
1856 if (canvas_nav_)
1857 canvas_nav_->HandleMapInteraction();
1858}
1859
1861 if (canvas_nav_)
1862 canvas_nav_->ScrollBlocksetCanvasToCurrentTile();
1863}
1864
1866 int quadrant) {
1867 if (rom_ == nullptr || !rom_->is_loaded() || game_data_ == nullptr ||
1868 !overworld_.is_loaded()) {
1869 return absl::FailedPreconditionError("Overworld ROM session is not loaded");
1870 }
1871 if (tile16_id < 0 ||
1872 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
1873 return absl::InvalidArgumentError("Selected Tile16 is unavailable");
1874 }
1875 auto* manager = static_cast<EditorManager*>(dependencies_.custom_data);
1876 if (manager == nullptr) {
1877 return absl::FailedPreconditionError("Graphics editor is unavailable");
1878 }
1880 const int source_map_id = current_map_;
1883 const auto* map = overworld_.overworld_map(source_map_id);
1884 if (map == nullptr || !map->is_built()) {
1885 return absl::FailedPreconditionError("Selected overworld map is not built");
1886 }
1887 if (map->HasGraphicsPreview()) {
1888 return absl::UnimplementedError(
1889 "Project graphics overrides need an explicit editable source map");
1890 }
1892 *map, overworld_.tiles16()[tile16_id], tile16_id, quadrant, game_data_);
1893 if (!context.ok()) {
1894 return context.status();
1895 }
1896 // Keep the source map's resolved assignments, palette, parent, and special
1897 // area state. The copied renderer reads live tile/map definitions and every
1898 // watched sheet from this ROM session on each preview refresh.
1899 auto preview_map = std::make_shared<zelda3::OverworldMap>(*map);
1900 const auto* owner = game_data_;
1901 context->resolve_live_source = [this, owner, source_map_id, tile16_id,
1902 quadrant](GraphicsEditContext& fresh) {
1903 if (game_data_ != owner || !overworld_.is_loaded() ||
1904 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
1905 return absl::FailedPreconditionError("Overworld source session changed");
1906 }
1908 overworld_.SyncAreaProperties(source_map_id);
1909 auto* live = overworld_.mutable_overworld_map(source_map_id);
1910 if (!live || live->HasGraphicsPreview()) {
1911 return absl::UnimplementedError(
1912 "Project graphics overrides have no writable source");
1913 }
1914 live->LoadAreaGraphics();
1915 RETURN_IF_ERROR(live->LoadPalette());
1916 auto resolved = MakeOverworldGraphicsEditContext(
1917 *live, overworld_.tiles16()[tile16_id], tile16_id, quadrant, owner);
1918 if (!resolved.ok())
1919 return resolved.status();
1920 fresh = std::move(*resolved);
1921 return absl::OkStatus();
1922 };
1923 context->limitation += " Palette colors follow live edits.";
1924 context->refresh_palettes = [this, preview_map, owner, tile16_id, quadrant](
1925 GraphicsEditContext& source) -> absl::Status {
1926 if (game_data_ != owner || !overworld_.is_loaded() ||
1927 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
1928 return absl::FailedPreconditionError("Overworld source session changed");
1929 }
1930 RETURN_IF_ERROR(preview_map->LoadPalette());
1931 auto refreshed = MakeOverworldGraphicsEditContext(
1932 *preview_map, overworld_.tiles16()[tile16_id], tile16_id, quadrant,
1933 owner);
1934 if (!refreshed.ok())
1935 return refreshed.status();
1936 return UpdateGraphicsContextPalettes(source, std::move(*refreshed));
1937 };
1938 context->render_preview =
1939 [this, preview_map, owner,
1940 source_map_id]() -> absl::StatusOr<usage_preview::PreviewImage> {
1941 if (game_data_ != owner || !overworld_.is_loaded()) {
1942 return absl::FailedPreconditionError(
1943 "Overworld graphics source session has changed");
1944 }
1945 std::set<uint16_t> ids;
1946 for (int slot = 0; slot < 8; ++slot) {
1947 ids.insert(preview_map->static_graphics(slot));
1948 }
1949 ids.insert(preview_map->animated_sheet());
1950 preview_map->SetGraphicsSheetOverrides(
1952 RETURN_IF_ERROR(preview_map->BuildTileset());
1953 RETURN_IF_ERROR(preview_map->BuildTiles16Gfx(
1955 static_cast<int>(overworld_.tiles16().size())));
1956 const int world = source_map_id / 0x40;
1957 RETURN_IF_ERROR(preview_map->BuildBitmap(overworld_.GetMapTiles(world)));
1959 image.width = 512;
1960 image.height = 512;
1961 image.pixels = preview_map->bitmap_data();
1962 image.index0_transparent = false;
1963 const auto& palette = preview_map->current_palette();
1964 for (size_t i = 0; i < palette.size() && i < image.colors.size(); ++i) {
1965 const auto rgb = palette[i].rgb();
1966 image.colors[i] =
1967 SDL_Color{static_cast<uint8_t>(rgb.x), static_cast<uint8_t>(rgb.y),
1968 static_cast<uint8_t>(rgb.z), 255};
1969 }
1970 return image;
1971 };
1972 context->return_to_source = [this, manager, source_map_id, tile16_id]() {
1973 manager->SwitchToEditor(EditorType::kOverworld, true);
1974 SelectMapForEditing(source_map_id, false);
1975 RequestTile16Selection(tile16_id);
1979 }
1980 };
1981 auto* editor_set = manager->GetCurrentEditorSet();
1982 if (editor_set == nullptr || editor_set->GetGraphicsEditor() == nullptr) {
1983 return absl::FailedPreconditionError("Graphics editor is unavailable");
1984 }
1985 RETURN_IF_ERROR(manager->EnsureEditorAssetsLoaded(EditorType::kGraphics));
1987 editor_set->GetGraphicsEditor()->OpenInContext(std::move(*context)));
1988 manager->SwitchToEditor(EditorType::kGraphics, true);
1989 return absl::OkStatus();
1990}
1991
1993 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1994 return;
1995 }
1996
1997 if (!rom_ || !rom_->is_loaded() || !all_gfx_loaded_ ||
1999 current_tile16_ = tile_id;
2001 return;
2002 }
2003
2005
2006 if (tile_id == tile16_editor_.current_tile16()) {
2007 current_tile16_ = tile_id;
2008 auto status = tile16_editor_.SetCurrentTile(tile_id);
2009 if (!status.ok()) {
2010 LOG_WARN("OverworldEditor", "Failed to refresh selected Tile16 %d: %s",
2011 tile_id, status.message().data());
2012 }
2013 return;
2014 }
2015
2018}
2019
2022 lyra_loaded_rom_sha1_.clear();
2023 if (is_lyra_preview()) {
2024 show_overlay_preview_ = true;
2025 }
2026
2027 // Unregister palette listener
2028 if (palette_listener_id_ >= 0) {
2031 }
2032
2034 current_graphics_set_.clear();
2035 all_gfx_loaded_ = false;
2036 map_blockset_loaded_ = false;
2037 return absl::OkStatus();
2038}
2039
2041 if (canvas_nav_)
2042 canvas_nav_->UpdateBlocksetSelectorState();
2043}
2044
2046 const int next =
2047 std::clamp(current_tile16_ + delta, 0, zelda3::kNumTile16Individual - 1);
2049}
2050
2054
2058
2067
2076
2078 const int world = std::clamp(current_world_, 0, 2);
2079 const int local = current_map_ - world * 0x40;
2080 const int rows = world == 2 && !overworld_.is_lyra_preview() ? 4 : 8;
2081 const int x = local % 8 + dx;
2082 const int y = local / 8 + dy;
2083 if (x < 0 || x >= 8 || y < 0 || y >= rows) {
2084 return;
2085 }
2086 const int target = world * 0x40 + y * 8 + x;
2087 if (target >= overworld_.inspected_map_count()) {
2088 return;
2089 }
2090 SelectMapForEditing(target, /*respect_pin=*/false);
2091 if (canvas_nav_) {
2092 // Keep the keyboard choice until the mouse moves, then resume following
2093 // the cursor (unless pinned).
2094 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
2095 canvas_nav_->CenterOnMap(target);
2096 }
2097}
2098
2100 if (map_id < 0 || map_id >= overworld_.inspected_map_count()) {
2101 return;
2102 }
2103 if (!overworld_.is_loaded()) {
2104 set_current_map(map_id);
2105 return;
2106 }
2107 const int world = std::clamp(map_id / 0x40, 0, 2);
2108 if (world != current_world_) {
2109 SwitchToWorld(world);
2110 }
2111 SelectMapForEditing(map_id, /*respect_pin=*/false);
2112 if (canvas_nav_) {
2113 // Same as keyboard navigation: keep the choice until the mouse moves.
2114 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
2115 canvas_nav_->CenterOnMap(map_id);
2116 }
2117}
2118
2119void OverworldEditor::LocateOverworldTile(int map_id, int world_x,
2120 int world_y) {
2121 JumpToMap(map_id);
2122 if (canvas_nav_) {
2123 canvas_nav_->CenterOnWorldTile(world_x, world_y);
2124 }
2125}
2126
2137
2139 if (is_lyra_preview()) {
2140 return;
2141 }
2142 entity_edit_mode_ = mode;
2143 if (mode != EntityEditMode::NONE) {
2144 // Entity work happens with the select tool.
2147 }
2148}
2149
2151 auto config = ow_map_canvas_.GetConfig();
2152 config.enable_grid = !config.enable_grid;
2154}
2155
2159
2161 auto* window_manager = dependencies_.window_manager;
2162 if (!window_manager) {
2163 return;
2164 }
2165 const size_t session_id = window_manager->GetActiveSessionId();
2166 window_manager->OpenWindow(session_id, OverworldPanelIds::kMapProperties);
2167 window_manager->MarkWindowRecentlyUsed(OverworldPanelIds::kMapProperties);
2168 // Bring a docked-but-hidden tab to front. No-op until the window exists.
2169 const std::string window_name = window_manager->GetWorkspaceWindowName(
2171 if (!window_name.empty()) {
2172 ImGui::SetWindowFocus(window_name.c_str());
2173 }
2174}
2175
2177 if (!status_bar)
2178 return;
2179 status_bar->SetCustomSegment(
2180 "Map",
2184 status_bar->SetCustomSegment("Tile16",
2185 absl::StrFormat("0x%03X", current_tile16_));
2186
2187 const char* mode_label;
2188 switch (entity_edit_mode_) {
2190 mode_label = "Entrances";
2191 break;
2193 mode_label = "Exits";
2194 break;
2196 mode_label = "Items";
2197 break;
2199 mode_label = "Sprites";
2200 break;
2202 mode_label = "Transports";
2203 break;
2205 mode_label = "Music";
2206 break;
2208 switch (current_mode) {
2209 case EditingMode::MOUSE:
2210 mode_label = "Mouse";
2211 break;
2213 mode_label = "Draw";
2214 break;
2216 mode_label = "Fill";
2217 break;
2218 default:
2219 mode_label = "Draw";
2220 break;
2221 }
2222 break;
2223 default:
2224 mode_label = "Draw";
2225 break;
2226 }
2227 status_bar->SetEditorMode(mode_label);
2228}
2229
2231 EditorContextSnapshot snapshot;
2232 snapshot.category = "Overworld";
2233 snapshot.semantic_owner = absl::StrFormat("overworld.map.%03X", current_map_);
2234
2235 if (!overworld_.is_loaded() || current_map_ < 0) {
2236 snapshot.title = "Overworld";
2237 snapshot.subtitle = "Load a ROM to inspect map context";
2238 return snapshot;
2239 }
2240
2243 snapshot.title = map.map_title;
2244 snapshot.subtitle =
2245 absl::StrFormat("%s · %s", map.world_label, map.area_size_label);
2246 snapshot.metadata = {
2247 {.id = "map", .label = "Map", .value = map.map_id_label},
2248 {.id = "parent", .label = "Parent", .value = map.parent_label},
2249 {.id = "tile16",
2250 .label = "Tile16",
2251 .value = absl::StrFormat("0x%03X", current_tile16_)},
2252 {.id = "graphics", .label = "Graphics", .value = map.area_gfx_label},
2253 {.id = "palette", .label = "Palette", .value = map.area_palette_label},
2254 };
2255
2256 const int owner_map = current_parent_;
2257 const auto belongs_to_area = [owner_map](const auto& entity) {
2258 return static_cast<int>(entity.map_id_) == owner_map;
2259 };
2260 const auto sprites = overworld_.sprites(game_state_);
2261 const auto items = overworld_.all_items();
2262 const auto* exits = overworld_.exits();
2263 snapshot.counts = {
2264 {.id = "sprites",
2265 .label = "Sprites",
2266 .value = std::to_string(std::count_if(
2267 sprites.begin(), sprites.end(),
2268 [owner_map](const zelda3::Sprite& sprite) {
2269 return static_cast<int>(sprite.map_id()) == owner_map;
2270 }))},
2271 {.id = "items",
2272 .label = "Items",
2273 .value = std::to_string(
2274 std::count_if(items.begin(), items.end(), belongs_to_area))},
2275 {.id = "entrances",
2276 .label = "Entrances",
2277 .value = std::to_string(std::count_if(overworld_.entrances().begin(),
2278 overworld_.entrances().end(),
2279 belongs_to_area))},
2280 {.id = "exits",
2281 .label = "Exits",
2282 .value = exits ? std::to_string(std::count_if(
2283 exits->begin(), exits->end(), belongs_to_area))
2284 : "0"},
2285 };
2286
2287 snapshot.has_pending_changes = rom_ != nullptr && rom_->dirty();
2288 snapshot.pending_label =
2289 snapshot.has_pending_changes ? "ROM buffer has pending changes" : "";
2290 if (current_map_lock_) {
2291 snapshot.diagnostics.push_back({
2292 .id = "map_pinned",
2294 .message = "Map context is pinned; cursor hover will not replace it.",
2295 });
2296 }
2297 if (!selected_tile16_ids_.empty()) {
2298 snapshot.diagnostics.push_back({
2299 .id = "tile_selection",
2301 .message = absl::StrFormat("%zu Tile16 cells selected",
2302 selected_tile16_ids_.size()),
2303 });
2304 }
2305
2306 snapshot.capabilities = {
2307 "overworld.properties",
2308 "overworld.entities",
2309 "overworld.tile16",
2310 };
2311 snapshot.actions = {
2312 {.id = "open_properties",
2313 .label = "Map Properties",
2314 .target = "overworld.properties"},
2315 {.id = "open_entities",
2316 .label = "Entity Workbench",
2317 .target = "overworld.entity_workbench"},
2318 {.id = "open_tile16",
2319 .label = "Tile16 Selector",
2320 .target = "overworld.tile16_selector"},
2321 };
2322 if (const auto* map = overworld_.overworld_map(current_map_)) {
2324 if (dependencies_.project != nullptr) {
2326 }
2328 snapshot, zelda3::DescribeOverworldGraphicsContext(*map, identity));
2329 }
2330 return snapshot;
2331}
2332
2333} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
absl::Status CheckLayoutWritable() const
Definition rom.cc:429
rom::LyraLayoutInspection InspectLyraLayout() const
Definition rom.cc:418
void set_dirty(bool dirty)
Definition rom.h:189
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool dirty() const
Definition rom.h:188
bool is_loaded() const
Definition rom.h:187
static Flags & get()
Definition features.h:119
bool loaded() const
Check if the manifest has been loaded.
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:364
UndoManager undo_manager_
Definition editor.h:371
EditorDependencies dependencies_
Definition editor.h:370
EditorType type() const
Definition editor.h:343
void SetHostSurfaceHint(std::string hint)
Main UI class for editing overworld maps in A Link to the Past.
void LocateOverworldTile(int map_id, int world_x, int world_y)
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_
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > maps_bmp_
EditorContextSnapshot BuildContextSnapshot() const override
std::optional< OverworldUndoPoint > current_paint_operation_
absl::Status BeginSaveTransaction() override
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)
void RequestItemInsertPopup(const std::optional< OverworldContextTarget > &target)
std::optional< zelda3::OverworldItem > selected_item_identity_
absl::Status RefreshMapPalette(int map_id=-1)
std::unique_ptr< zelda3::OverworldUpgradeSystem > upgrade_system_
absl::Status SyncProjectGraphicsPreview(bool force=false)
void HandleEntityInsertion(const std::string &entity_type, const OverworldContextTarget &target)
Handle entity insertion from context menu.
void NotifyEntityModified(zelda3::GameEntity *entity)
Notify that an entity has been modified, marking ROM as dirty.
absl::Status CheckRomReplacement(const Rom &replacement) const
std::unique_ptr< MapRefreshCoordinator > map_refresh_
void SelectAdjacentMap(int dx, int dy)
Alt+Arrow: select the neighbouring screen in the world grid and center it.
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 PrimeWorldMaps(int world, bool process_texture_queue=false)
bool SampleContextTile16(const OverworldContextTarget &target)
absl::Status ApplyOverworldPropertyEdits(const std::vector< OverworldPropertyEdit > &edits, const std::string &description, bool record_undo=true)
void set_current_map(int map_id)
Set the current map for editing (also updates world)
void RefreshOverworldMapOnDemand(int map_index)
void RefreshMapsForSheetEdits()
Mark maps for a graphics rebuild when a sheet they use changed in the session's sheet store (an unsav...
std::unique_ptr< OverworldSidebar > sidebar_
std::optional< int > pending_tile16_selection_after_gfx_
void HandleTile16Edit(const OverworldContextTarget &target)
Handle tile16 editing from context menu (MOUSE mode) Opens the Tile16 captured at menu-open through t...
void SetEditingMode(EditingMode mode)
Tool change from the toolbar or keys 1/2: also leaves entity modes.
absl::Status RenameProjectResourceLabelWithUndo(const std::string &type, int id, const std::string &label)
zelda3::OverworldItem * GetSelectedItem()
Resolve selected item identity to the current live item pointer.
bool NormalizeCurrentSelectionState()
Handle overworld keyboard shortcuts and edit-mode hotkeys.
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_
std::optional< OverworldEntityInsertionRequest > pending_entity_insertion_
absl::Status Load() override
void EnsureMapTexture(int map_index)
Ensure a specific map has its texture created.
std::vector< gfx::Bitmap > sprite_previews_
absl::Status OpenGraphicsForTile16Quadrant(int tile16_id, int quadrant)
std::unique_ptr< CanvasNavigationManager > canvas_nav_
bool NudgeSelectedItem(int delta_x, int delta_y)
Move selected item by signed pixel deltas.
const gfx::Bitmap * AreaScreenBitmap(int map_id)
std::unique_ptr< OverworldToolbar > toolbar_
void ProcessDeferredTextures()
Create textures for deferred map bitmaps on demand.
absl::Status OpenGraphicsForContextTile16(const OverworldContextTarget &target)
std::unique_ptr< gui::TileSelectorWidget > blockset_selector_
void RefreshMapProperties(int map_id=-1)
absl::Status Paste() override
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset canvas to show the current selected tile16.
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 SetEntityEditMode(EntityEditMode mode)
Entity focus (entrances/exits/items/sprites); NONE = all entities.
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)
std::string GetDisplayString(const std::string &name) const
Live, platform-formatted binding for a shortcut name.
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 BindDocument(std::vector< gfx::Tile16 > *definitions, UndoManager *history, std::function< void()> before_edit={})
void SetOpenGraphicsForQuadrant(std::function< absl::Status(int, int)> callback)
void set_on_document_changed(std::function< void(const std::vector< Tile16Commit > &)> callback)
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)
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, std::string action_label={}, std::function< void()> on_action=nullptr)
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 QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:580
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:587
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
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:198
TextureHandle texture() const
Definition bitmap.h:410
bool is_active() const
Definition bitmap.h:414
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
SDL_Surface * surface() const
Definition bitmap.h:409
static PaletteManager & Get()
Get the singleton instance.
RAII timer for automatic timing management.
auto global_scale() const
Definition canvas.h:413
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:385
void ApplyConfigSnapshot(const CanvasConfig &snapshot)
Definition canvas.cc:1038
void ClearContextMenuItems()
Definition canvas.cc:994
CanvasConfig & GetConfig()
Definition canvas.h:243
bool IsMouseHovering() const
Definition canvas.h:354
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:810
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:136
void SyncAreaProperties(int map_index)
Copy the area parent's render settings (main palette, animated GFX, tile GFX groups,...
const std::vector< uint8_t > & current_graphics() const
Definition overworld.h:764
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:780
absl::Status SaveMap16Tiles()
Save tile16 definitions to ROM.
auto sprites(int state) const
Definition overworld.h:754
auto all_items() const
Definition overworld.h:833
const std::vector< gfx::Tile16 > & tiles16() const
Definition overworld.h:751
bool is_lyra_preview() const
Definition overworld.h:430
auto is_loaded() const
Definition overworld.h:803
int inspected_map_count() const
Definition overworld.h:433
absl::Status CreateTile32Tilemap()
Build tile32 tilemap from current tile16 data.
auto overworld_map(int i) const
Definition overworld.h:737
const std::vector< uint8_t > & tile16_blockset_data() const
Definition overworld.h:792
void SetGraphicsPreview(OverworldGraphicsPreview preview)
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:809
absl::StatusOr< OverworldSpriteSavePlan > PrepareSpriteSave() const
auto mutable_overworld_map(int i)
Definition overworld.h:743
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.
absl::Status LoadLyraTerrainPreview(Rom *rom)
Definition overworld.cc:242
absl::Status SaveOverworldMaps()
Save compressed map tile data to ROM.
auto mutable_sprites(int state)
Definition overworld.h:759
void set_game_state(int state)
Definition overworld.cc:132
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:770
absl::Status SaveMap16Expanded()
Save expanded tile16 definitions (v1+ ROMs)
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:721
absl::Status SaveMusic()
Save per-area music IDs.
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
#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:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< zelda3::OverworldGraphicsPreview > LoadOverworldGraphicsPreview(const std::string &filename)
bool ItemIdentityMatchesForUndo(const zelda3::OverworldItem &lhs, const zelda3::OverworldItem &rhs)
bool ItemSnapshotsEqual(const OverworldItemsSnapshot &lhs, const OverworldItemsSnapshot &rhs)
SheetPixels SnapshotSessionSheets(const std::set< uint16_t > &ids, const zelda3::GameData *game_data)
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.
absl::Status UpdateGraphicsContextPalettes(GraphicsEditContext &context, GraphicsEditContext refreshed)
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)
void AppendResolvedGraphicsContext(EditorContextSnapshot &snapshot, const zelda3::ResolvedGraphicsContext &context)
constexpr int kTile16Size
absl::StatusOr< GraphicsEditContext > MakeOverworldGraphicsEditContext(const zelda3::OverworldMap &map, const gfx::Tile16 &tile16, int tile16_id, int quadrant, const zelda3::GameData *owner)
OverworldMapMetadata BuildOverworldMapMetadata(const zelda3::Overworld &overworld, const Rom *rom, const project::YazeProject *project, int map_id, int game_state)
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)
std::string ComputeSha1Hex(const uint8_t *data, size_t size)
Compute SHA-1 hash of data, return lowercase hex string (40 chars).
Definition rom_hash.cc:234
constexpr int kNumTile16Individual
Definition overworld.h:243
constexpr int kNumOverworldMaps
Definition common.h:85
absl::Status ApplyOverworldSpriteSave(Rom &rom, const OverworldSpriteSavePlan &plan)
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:89
OverworldRomProfile DetectOverworldRomProfile(const Rom &rom)
Definition overworld.h:316
absl::StatusOr< OverworldEntranceTileTypes > LoadEntranceTileTypes(Rom *rom)
ResolvedGraphicsContext DescribeOverworldGraphicsContext(const OverworldMap &map, const GraphicsContextIdentity &identity)
#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).
std::function< void()> open_map_properties
Opens (and focuses) the Map Properties window. Double-click target.
Shared state pointers that the navigation manager reads/writes.
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp
std::unique_ptr< gui::TileSelectorWidget > * blockset_selector
std::vector< EditorContextValue > counts
std::vector< EditorContextAction > actions
std::vector< EditorContextDiagnostic > diagnostics
std::vector< EditorContextValue > metadata
project::YazeProject * project
Definition editor.h:174
SharedClipboard * shared_clipboard
Definition editor.h:187
gfx::IRenderer * renderer
Definition editor.h:192
ShortcutManager * shortcut_manager
Definition editor.h:186
WorkspaceWindowManager * window_manager
Definition editor.h:182
Context struct holding all data dependencies for map refresh operations. All pointers/references must...
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp
std::function< void(int map_index)> ensure_map_texture
Callback to ensure a map texture is created (stays in OverworldEditor)
std::function< void(int map_index)> refresh_map_on_demand
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * 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 * kMapProperties
static constexpr const char * kItemList
static constexpr const char * kTile16Editor
std::vector< int > overworld_tile16_ids
Definition editor.h:120
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)> select_map_for_editing
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::kExpandedMapCount > * maps_bmp
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
RomWritePolicy write_policy
Definition project.h:225
core::HackManifest hack_manifest
Definition project.h:360
std::string overworld_graphics_preview
Definition project.h:320