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
104class LyraTerrainPaintAction final : public UndoAction {
105 public:
106 LyraTerrainPaintAction(uint16_t before, uint16_t after,
107 std::function<absl::Status(uint16_t)> apply)
108 : before_(before), after_(after), apply_(std::move(apply)) {}
109 absl::Status Undo() override { return apply_(before_); }
110 absl::Status Redo() override { return apply_(after_); }
111 std::string Description() const override { return "Paint Lyra A0 terrain"; }
112 size_t MemoryUsage() const override { return sizeof(*this); }
113
114 private:
115 uint16_t before_;
116 uint16_t after_;
117 std::function<absl::Status(uint16_t)> apply_;
118};
119
121 const zelda3::OverworldItem& rhs) {
122 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
123 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.bg2_ == rhs.bg2_;
124}
125
127 const OverworldItemsSnapshot& rhs) {
128 if (lhs.items.size() != rhs.items.size()) {
129 return false;
130 }
131
132 for (size_t i = 0; i < lhs.items.size(); ++i) {
133 if (!ItemIdentityMatchesForUndo(lhs.items[i], rhs.items[i])) {
134 return false;
135 }
136 }
137
138 if (lhs.selected_item_identity.has_value() !=
139 rhs.selected_item_identity.has_value()) {
140 return false;
141 }
142 if (!lhs.selected_item_identity.has_value()) {
143 return true;
144 }
145
148}
149
150} // namespace
151
153 if (palette_listener_id_ >= 0) {
156 }
157
164
165 // These store callbacks and raw pointers into the editor. Release them while
166 // all editor-owned state is still alive rather than during member teardown.
167 sidebar_.reset();
169}
170
175
177 return lyra_canvas_.loaded() && overworld_.is_loaded() &&
178 !overworld_.is_lyra_preview() && map_id >= 0xA0 && map_id < 0xC0;
179}
180
182 int& current_map, int map_count) {
183 map_count = std::clamp(map_count, 1, zelda3::kExpandedMapCount);
184 const int clamped_world = std::clamp(current_world, 0, 2);
185 int normalized_map = current_map;
186 if (normalized_map < 0 || normalized_map >= map_count) {
187 normalized_map = std::min(clamped_world * 0x40, map_count - 1);
188 }
189
190 const int normalized_world = std::clamp(normalized_map / 0x40, 0, 2);
191 const bool changed =
192 normalized_world != current_world || normalized_map != current_map;
193 current_world = normalized_world;
194 current_map = normalized_map;
195 return changed;
196}
197
199 // Initialize renderer from dependencies
201
203 "Gfx Groups: selection syncs with the Graphics editor for this ROM "
204 "session "
205 "(this surface uses its own preview canvases).");
206
208
209 // Initialize MapRefreshCoordinator (must be before callbacks that use it)
211
212 if (rom_) {
213 upgrade_system_ = std::make_unique<zelda3::OverworldUpgradeSystem>(*rom_);
214 }
216 std::make_unique<EntityMutationService>(overworld_);
217
219
220 // Note: All WindowContent instances now self-register via REGISTER_PANEL macro
221 // and use ContentRegistry::Context to access the current editor.
222 // See comment at include section for the current set of view wrappers.
223
224 // Original initialization code below:
225 // Initialize MapPropertiesSystem with canvas and bitmap data
226 // Initialize cards
227 usage_stats_card_ = std::make_unique<UsageStatisticsCard>(&overworld_);
228 debug_window_card_ = std::make_unique<DebugWindowCard>();
229
230 map_properties_system_ = std::make_unique<MapPropertiesSystem>(
232
233 // Set up refresh callbacks for MapPropertiesSystem
234 map_properties_system_->SetRefreshCallbacks(
235 [this](int map_id) { this->RefreshMapProperties(map_id); },
236 [this]() { this->RefreshOverworldMap(); },
237 [this](int map_id) -> absl::Status {
238 return this->RefreshMapPalette(map_id);
239 },
240 [this]() -> absl::Status { return this->RefreshTile16Blockset(); },
241 [this](int map_index) { this->ForceRefreshGraphics(map_index); });
242 map_properties_system_->SetMapSelectionCallback(
243 [this](int map_index, bool respect_pin) {
244 this->SelectMapForEditing(map_index, respect_pin);
245 });
246 map_properties_system_->SetPropertyEditCallback(
247 [this](const OverworldPropertyEdit& edit) {
248 return this->ApplyOverworldPropertyEdit(edit);
249 });
250 map_properties_system_->SetPropertyEditBatchCallback(
251 [this](const std::vector<OverworldPropertyEdit>& edits,
252 const std::string& description) {
253 return this->ApplyOverworldPropertyEdits(edits, description);
254 });
255 map_properties_system_->SetResourceLabelEditCallback(
256 [this](const std::string& type, int id, const std::string& label) {
257 return this->RenameProjectResourceLabelWithUndo(type, id, label);
258 });
259
260 // Initialize OverworldSidebar
261 sidebar_ = std::make_unique<OverworldSidebar>(&overworld_, rom_,
263
264 // Initialize OverworldEntityRenderer for entity visualization
265 entity_renderer_ = std::make_unique<OverworldEntityRenderer>(
267
268 sidebar_->SetRenameLabelCallback(
269 [this](const std::string& type, int id, const std::string& label) {
270 return this->RenameProjectResourceLabelWithUndo(type, id, label);
271 });
272
273 // Initialize canvas navigation and drawing tools. The sidebar also renders
274 // quick appearance fields above the canvas.
275 toolbar_ = std::make_unique<OverworldToolbar>();
276 toolbar_->on_story_changed = [this](int state) {
278 };
279 sidebar_->on_story_changed = toolbar_->on_story_changed;
280 sidebar_->draw_project_tools = [this](int map_id, int& state) {
281 if (lyra_canvas_.loaded())
284 show_entities_ = true;
285 };
286 toolbar_->on_world_changed = [this](int world) {
287 SwitchToWorld(world);
288 };
289 toolbar_->on_set_mode = [this](EditingMode mode) {
290 SetEditingMode(mode);
291 };
292 toolbar_->on_set_entity_mode = [this](EntityEditMode mode) {
293 SetEntityEditMode(mode);
294 };
295 toolbar_->on_insert_item = [this]() {
296 RequestItemInsertPopup(std::nullopt);
297 };
298 toolbar_->on_open_map_properties = [this]() {
300 };
301 toolbar_->on_toggle_overlay_preview = [this]() {
303 };
304 toolbar_->is_overlay_preview_enabled = [this]() {
306 };
307 toolbar_->on_toggle_grid = [this]() {
308 ToggleGrid();
309 };
310 toolbar_->is_grid_visible = [this]() {
311 return grid_visible();
312 };
313 toolbar_->on_toggle_entities = [this]() {
315 };
316 toolbar_->are_entities_visible = [this]() {
317 return entities_visible();
318 };
319 toolbar_->on_zoom_in = [this]() {
320 ZoomIn();
321 };
322 toolbar_->on_zoom_out = [this]() {
323 ZoomOut();
324 };
325 toolbar_->on_zoom_fit = [this]() {
326 ZoomToFit();
327 };
328 toolbar_->on_center_map = [this]() {
330 };
331 toolbar_->get_zoom = [this]() {
333 };
334 toolbar_->on_toggle_overlay_preview = [this]() {
336 };
337 toolbar_->is_overlay_preview_enabled = [this]() {
339 };
340 toolbar_->on_upgrade_rom_version = [this](int) {
341 ImGui::OpenPopup("UpgradeROMVersion");
342 };
343
344 // Initialize OverworldCanvasRenderer for canvas and panel drawing
345 canvas_renderer_ = std::make_unique<OverworldCanvasRenderer>(this);
346 map_properties_system_->SetOpenMapPropertiesCallback(
347 [this]() { OpenMapPropertiesWindow(); });
348 map_properties_system_->SetContextNavigationCallbacks(
349 [this]() { ResetOverworldView(); }, [this]() { ZoomIn(); },
350 [this]() { ZoomOut(); }, [this]() { ZoomToFit(); },
351 [this]() { CenterOverworldView(); });
352 map_properties_system_->SetContextViewToggles(
353 {[this]() { return grid_visible(); }, [this]() { ToggleGrid(); }},
354 {[this]() { return entities_visible(); },
355 [this]() { ToggleEntityVisibility(); }},
356 {[this]() { return show_overlay_preview_; },
358 map_properties_system_->SetCurrentMapProvider(
359 [this]() { return current_map_; });
360 map_properties_system_->SetMapJumpCallback(
361 [this](int map_id) { JumpToMap(map_id); });
362 map_properties_system_->SetShortcutHintProvider(
363 [this](const char* name) -> std::string {
366 : std::string();
367 });
368
372}
373
376 sink.on_set_editor_mode = [this](EditingMode mode) {
377 SetEditingMode(mode);
378 };
379 sink.on_set_entity_mode = [this](EntityEditMode mode) {
380 SetEntityEditMode(mode);
381 };
382 sink.on_pick_tile_from_hover = [this]() {
384 };
385 sink.on_duplicate_selected = [this]() {
386 (void)DuplicateSelectedItem();
387 };
388 sink.on_nudge_selected = [this](int delta_x, int delta_y, bool shift_held) {
389 const int step = shift_held ? 16 : 1;
390 (void)this->NudgeSelectedItem(delta_x * step, delta_y * step);
391 };
392 sink.on_entity_context_menu = [this](zelda3::GameEntity* entity) {
394 auto* workbench = static_cast<OverworldEntityWorkbench*>(
396 "overworld.entity_workbench"));
397 if (workbench) {
398 workbench->OpenContextMenuFor(entity);
399 }
400 }
401 };
402 sink.on_entity_double_click = [this](zelda3::GameEntity* entity) {
403 if (!entity) {
404 return;
405 }
406 switch (entity->entity_type_) {
408 this->jump_to_tab_ =
409 static_cast<zelda3::OverworldExit*>(entity)->room_id_;
410 break;
413 static_cast<zelda3::OverworldEntrance*>(entity)->entrance_id_);
414 break;
415 default:
416 break;
417 }
418 };
419 sink.can_edit_items = [this]() {
420 // Items mode, or no entity focus with an item selected, in select mode.
422 return false;
423 }
425 return true;
426 }
428 GetSelectedItem() != nullptr;
429 };
430 sink.on_undo = [this]() {
431 status_ = Undo();
432 };
433 sink.on_redo = [this]() {
434 status_ = Redo();
435 };
437 std::make_unique<OverworldInteractionCoordinator>(std::move(sink));
438}
439
443 deps.overworld = &overworld_;
444 deps.maps_bmp = &maps_bmp_;
451 deps.rom = rom_;
453
454 TilePaintingCallbacks callbacks;
455 callbacks.create_undo_point = [this](int map_id, int world, int x, int y,
456 int old_tile_id) {
457 this->CreateUndoPoint(map_id, world, x, y, old_tile_id);
458 };
459 callbacks.finalize_paint_operation = [this]() {
461 };
462 callbacks.refresh_overworld_map_on_demand = [this](int map_index) {
463 this->RefreshOverworldMapOnDemand(map_index);
464 };
465 callbacks.scroll_blockset_to_current_tile = [this]() {
467 };
468 callbacks.request_tile16_selection = [this](int tile_id) {
469 this->RequestTile16Selection(tile_id);
470 };
471 callbacks.select_map_for_editing = [this](int map_id) {
472 if (map_id == current_map_) {
473 return;
474 }
475 // Respects the pin and refreshes the Tile16 selector palette for the
476 // newly selected map. A same-map stroke does not rebuild the atlas.
477 SelectMapForEditing(map_id, true);
478 };
479 callbacks.is_project_tile = [this](int world, int x, int y) {
480 return lyra_canvas_.loaded() && world == 2 && x >= 0 && x < 256 &&
481 y >= 128 && y < 256;
482 };
483 callbacks.read_project_tile = [this](int world, int x,
484 int y) -> absl::StatusOr<uint16_t> {
485 if (!lyra_canvas_.loaded() || world != 2)
486 return absl::FailedPreconditionError("Lyra terrain is unavailable");
487 return lyra_canvas_.document()->tile(x, y - 128);
488 };
489 callbacks.paint_project_pattern = [this](const TileBrush& brush, int x, int y,
490 int width, int height,
491 bool finalize) {
492 status_ = lyra_canvas_.PaintPattern(brush, x, y - 128, width, height,
493 finalize, undo_manager_);
494 return status_;
495 };
496 callbacks.finalize_project_paint = [this]() {
498 };
499 callbacks.project_tile_atlas = [this](int map_id) -> const gfx::Bitmap* {
500 return lyra_canvas_.loaded() ? &lyra_canvas_.MapAtlas(map_id) : nullptr;
501 };
502 callbacks.allocated_rows = [this](int world) {
503 return world == 2 && !lyra_canvas_.loaded() ? 4 : 8;
504 };
505
506 tile_painting_ = std::make_unique<TilePaintingManager>(deps, callbacks);
507}
508
512 ctx.overworld = &overworld_;
513 ctx.rom = rom_;
522 ctx.maps_bmp = &maps_bmp_;
525
527 callbacks.displayed_map_count = [this]() {
528 return DisplayedMapCount();
529 };
530 callbacks.is_project_terrain_map = [this](int map_id) {
531 return IsProjectTerrainMap(map_id);
532 };
533 callbacks.refresh_overworld_map = [this]() {
534 this->RefreshOverworldMap();
535 };
536 callbacks.refresh_tile16_blockset = [this]() -> absl::Status {
537 return this->RefreshTile16Blockset();
538 };
539 callbacks.ensure_map_texture = [this](int map_index) {
540 this->EnsureMapTexture(map_index);
541 };
542 callbacks.select_map_for_editing = [this](int map_index, bool respect_pin) {
543 this->SelectMapForEditing(map_index, respect_pin);
544 };
545 callbacks.pick_tile16_from_hovered_canvas = [this]() -> bool {
546 return this->PickTile16FromHoveredCanvas();
547 };
548 callbacks.is_entity_hovered = [this]() -> bool {
549 return entity_renderer_ && entity_renderer_->hovered_entity() != nullptr;
550 };
551 callbacks.open_map_properties = [this]() {
553 };
554
555 canvas_nav_ = std::make_unique<CanvasNavigationManager>();
556 canvas_nav_->Initialize(ctx, callbacks);
557}
558
560 current_entity_ = entity;
561 if (!entity) {
563 return;
564 }
566 selected_item_identity_ = *static_cast<zelda3::OverworldItem*>(entity);
567 } else {
569 }
570}
571
577
579 const Rom& replacement) const {
580 if (!overworld_.is_loaded() ||
581 (!is_lyra_preview() && replacement.InspectLyraLayout().presence ==
583 (is_lyra_preview() &&
584 util::ComputeSha1Hex(replacement.data(), replacement.size()) ==
586 return absl::OkStatus();
587 }
588 return absl::FailedPreconditionError(
589 "Lyra canvas session replacement requires a fresh editor session");
590}
591
592absl::Status OverworldEditor::Load() {
593 gfx::ScopedTimer timer("OverworldEditor::Load");
594 if (graphics_source_ && graphics_source_->any_dirty())
595 return absl::FailedPreconditionError(
596 "Publish project graphics before reloading overworld data");
597
598 LOG_DEBUG("OverworldEditor", "Loading overworld.");
599 if (!rom_ || !rom_->is_loaded()) {
600 return absl::FailedPreconditionError("ROM not loaded");
601 }
602 if (lyra_canvas_.dirty()) {
603 return absl::FailedPreconditionError(
604 "Save the Lyra terrain draft before reloading overworld data");
605 }
606 const auto lyra = rom_->InspectLyraLayout();
607 if (lyra.presence == rom::LyraLayoutPresence::kInvalid) {
608 return absl::FailedPreconditionError(
609 "Invalid Lyra descriptor; overworld loading refused");
610 }
612 lyra.presence == rom::LyraLayoutPresence::kValid &&
615 // The manager may call Load again after restoring the same ROM bytes.
616 // Keep the bound maps, descriptor pointer, textures and undo state.
617 return absl::OkStatus();
618 }
619 if (overworld_.is_loaded() &&
620 (lyra.presence == rom::LyraLayoutPresence::kValid ||
622 return absl::FailedPreconditionError(
623 "Lyra canvas session replacement requires a fresh editor session");
624 }
627
629 dependencies_.window_manager->RegisterPanelAlias("overworld.debug_window",
630 "overworld.debug");
631 }
632
633 graphics_source_.reset();
634 graphics_source_project_ = nullptr;
637 graphics_import_plan_.reset();
638 graphics_source_open_ = false;
639
640 // Clear undo/redo state when loading new ROM data
643
647 all_gfx_loaded_ = true;
648 return absl::OkStatus();
649 }
651 [this]() {
652 FinishScratchEdit("Edit scratch layout");
653 lyra_canvas_.FinishStroke(undo_manager_);
654 FinalizePaintOperation();
655 });
660 const auto collision_types = overworld_.GetTile8CollisionTypes(current_map_);
661 if (collision_types.ok()) {
662 tile16_editor_.SetCollisionTypes(*collision_types);
663 } else {
664 LOG_WARN("OverworldEditor", "Collision labels unavailable: %s",
665 collision_types.status().ToString().c_str());
666 }
667
668 // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette
672 [this](int id) { current_tile16_ = id; });
674 [this](int tile16_id, int quadrant) {
675 return OpenGraphicsForTile16Quadrant(tile16_id, quadrant);
676 });
677 tile16_editor_.SetHoverDetailProvider([this](int tile_id) -> std::string {
678 const int count = GetTile16UsageCount(tile_id);
679 if (count <= 0) {
680 return {};
681 }
682 return absl::StrFormat("Placed %d times (%s)", count,
684 });
685
687 [this](const std::vector<Tile16Commit>&) {
688 if (map_refresh_) {
689 map_refresh_->InvalidateTile16Definitions();
692 }
693 });
694
695 // Set up entity insertion callback for MapPropertiesSystem
697 map_properties_system_->SetEntityCallbacks(
698 [this](const std::string& entity_type,
699 const OverworldContextTarget& target) {
700 HandleEntityInsertion(entity_type, target);
701 });
702
703 // Set up tile16 edit callback for context menu in MOUSE mode
704 map_properties_system_->SetTile16SampleCallback(
705 [this](const OverworldContextTarget& target) {
706 return SampleContextTile16(target);
707 });
708 map_properties_system_->SetTile16EditCallback(
709 [this](const OverworldContextTarget& target) {
710 HandleTile16Edit(target);
711 });
712 map_properties_system_->SetTile16GraphicsCallback(
713 [this](const OverworldContextTarget& target) {
715 });
716 }
717
719
720 // Register as palette listener to refresh graphics when palettes change
721 if (palette_listener_id_ < 0) {
723 [this](const std::string& group_name, int palette_index) {
724 if (!gfx::PaletteManager::Get().IsManaging(game_data_) ||
726 return;
727 }
728 status_ =
729 map_refresh_->RefreshPaletteUsers(group_name, palette_index);
730 });
731 LOG_DEBUG("OverworldEditor", "Registered as palette listener (ID: %d)",
733 }
734
735 all_gfx_loaded_ = true;
736
738 if (IsProjectTerrainMap(0xA0)) {
739 SelectMapForEditing(0xA0, false);
740 hovered_map_ = -1;
741 if (canvas_nav_) {
742 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
743 canvas_nav_->CenterOnMap(0xA0);
744 }
745 }
746
747 auto scratch_status = LoadScratchPad();
748 if (!scratch_status.ok()) {
749 LOG_WARN("OverworldEditor", "Failed to load scratch pad: %s",
750 std::string(scratch_status.message()).c_str());
751 }
752
754 const int pending = *pending_tile16_selection_after_gfx_;
756 RequestTile16Selection(pending);
757 }
758
759 return absl::OkStatus();
760}
761
763 state = std::clamp(state, 0, 2);
764 if (state == game_state_)
765 return;
768 SetCurrentEntity(nullptr);
769 }
770 game_state_ = state;
772 for (int id = 0; id < zelda3::kNumOverworldMaps; ++id) {
773 if (auto* map = overworld_.mutable_overworld_map(id)) {
774 map->set_game_state(state);
776 }
777 }
778 if (all_gfx_loaded_) {
781 }
782}
783
786 bool refresh_selection = false;
787 for (int map = 0; map < zelda3::kNumOverworldMaps; ++map) {
788 auto* overworld_map = overworld_.mutable_overworld_map(map);
789 if (overworld_map != nullptr) {
790 overworld_map->set_game_state(std::clamp(game_state_, 0, 2));
791 }
792 // A map already marked for refresh waits for its rebuild, which records
793 // the new sheet revisions; marking it again every frame would keep
794 // clearing the blockset cache.
795 if (overworld_map != nullptr && !maps_bmp_[map].modified() &&
796 overworld_map->SourceSheetsChanged()) {
798 refresh_selection |= map == current_map_;
799 }
800 }
801 if (refresh_selection) {
802 // Keep the selected map, Tile16 thumbnails and pixel-source metadata in
803 // agreement even when the map canvas is closed. Other maps refresh lazily.
806 }
807}
808
810 status_ = absl::OkStatus();
811
812 // Safety check: Ensure ROM is loaded and graphics are ready
813 if (!rom_ || !rom_->is_loaded()) {
815 return absl::OkStatus();
816 }
817
818 if (!all_gfx_loaded_) {
819 gui::DrawEmptyState(gui::EmptyLoading("Loading overworld graphics…"));
820 return absl::OkStatus();
821 }
822
826 return status_;
827 }
828
831
833
834 // Process deferred textures for smooth loading
836
837 // Show unsaved shared assignments and pixels in maps using those sources.
839
840 // Early return if window_manager is not available
841 // (panels won't be drawn without it, so no point continuing)
843 return status_;
844 }
845
847 return status_;
848 }
849
850 // ===========================================================================
851 // Main Overworld Canvas
852 // ===========================================================================
853 // The panels (Tile16 Selector, Area Graphics, etc.) are now managed by
854 // WindowContent/WorkspaceWindowManager and drawn automatically. This section only
855 // handles the main canvas and toolbar.
856
857 // ===========================================================================
858 // Non-Panel Windows (not managed by WindowContent system)
859 // ===========================================================================
860 // These are separate feature windows, not part of the panel system
861
862 // Custom Background Color Editor
864 ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
865 if (ImGui::Begin(ICON_MD_FORMAT_COLOR_FILL " Background Color",
867 if (rom_->is_loaded() && overworld_.is_loaded() &&
869 map_properties_system_->DrawCustomBackgroundColorEditor(
871 }
872 }
873 ImGui::End();
874 }
875
876 // Visual Effects Editor (Subscreen Overlays)
878 ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_FirstUseEver);
879 if (ImGui::Begin(ICON_MD_LAYERS " Visual Effects Editor###OverlayEditor",
881 if (rom_->is_loaded() && overworld_.is_loaded() &&
883 map_properties_system_->DrawOverlayEditor(current_map_,
885 }
886 }
887 ImGui::End();
888 }
889
890 // Note: Tile16 Editor is now managed as an WindowContent (Tile16EditorPanel)
891 // It uses UpdateAsPanel() which provides a context menu instead of MenuBar
892
893 if (auto* workbench = GetWorkbench()) {
894 workbench->ProcessPendingInsertion(entity_mutation_service_.get());
895 workbench->DrawPopups();
896 }
897
898 if (ImGui::BeginPopupModal("UpgradeROMVersion", nullptr,
899 ImGuiWindowFlags_AlwaysAutoResize)) {
900 ImGui::Text(ICON_MD_UPGRADE " Upgrade ROM to ZSCustomOverworld");
901 ImGui::Separator();
902 ImGui::TextWrapped(
903 tr("This will apply the ZSCustomOverworld ASM patch to your ROM,\n"
904 "enabling advanced overworld features."));
905 ImGui::Separator();
906
907 const uint8_t current_version =
909 : 0xFF;
910 ImGui::Text(tr("Current Version: %s"),
911 current_version == 0xFF
912 ? "Vanilla"
913 : absl::StrFormat("v%d", current_version).c_str());
914
915 static int target_version = 3;
916 ImGui::RadioButton(tr("v2 (Basic features)"), &target_version, 2);
917 ImGui::SameLine();
918 ImGui::RadioButton(tr("v3 (All features)"), &target_version, 3);
919
920 ImGui::Separator();
921
922 if (ImGui::Button(ICON_MD_CHECK " Apply Upgrade", ImVec2(150.0f, 0.0f))) {
923 auto upgrade_status =
924 upgrade_system_ != nullptr
925 ? upgrade_system_->ApplyZSCustomOverworldASM(target_version)
926 : absl::FailedPreconditionError("Upgrade system not initialized");
927 if (upgrade_status.ok()) {
928 status_ = Clear();
929 if (status_.ok()) {
930 status_ = Load();
931 }
932 if (status_.ok()) {
933 ImGui::CloseCurrentPopup();
934 }
935 } else {
936 LOG_ERROR("OverworldEditor", "Upgrade failed: %s",
937 upgrade_status.message().data());
938 }
939 }
940 ImGui::SameLine();
941 if (ImGui::Button(ICON_MD_CANCEL " Cancel", ImVec2(150.0f, 0.0f))) {
942 ImGui::CloseCurrentPopup();
943 }
944
945 ImGui::EndPopup();
946 }
947
948 // ===========================================================================
949 // Centralized Entity Interaction Logic (extracted to dedicated method)
950 // ===========================================================================
951 zelda3::GameEntity* hovered_entity =
952 entity_renderer_ ? entity_renderer_->hovered_entity() : nullptr;
954 interaction_coordinator_->Update(hovered_entity);
955 }
956
957 return absl::OkStatus();
958}
959
962
963 if (canvas_renderer_) {
964 canvas_renderer_->DrawOverworldCanvas();
965 }
966}
967
969 if (graphics_source_ && graphics_source_->any_dirty())
970 return absl::FailedPreconditionError(
971 "Publish project graphics before Save Project");
972 FinishScratchEdit("Edit scratch layout");
976}
977
979 const auto* previous = lyra_canvas_.document();
981 if (previous && previous != lyra_canvas_.document())
983 lyra_canvas_status_ = absl::OkStatus();
985 return absl::OkStatus();
986}
987
988absl::Status OverworldEditor::OpenLyraTerrain(const std::string& path) {
989 const auto* previous = lyra_canvas_.document();
991 if (previous && previous != lyra_canvas_.document())
993 lyra_canvas_status_ = absl::OkStatus();
995 return absl::OkStatus();
996}
997
999 project::YazeProject* project, const std::string& project_path) {
1000 if (graphics_source_ && graphics_source_->any_dirty())
1001 return absl::FailedPreconditionError(
1002 "Publish project graphics before Save Project As");
1004 return lyra_canvas_.SaveAs(project, project_path, overworld_);
1005}
1006
1009 hovered_map_ = -1;
1010 if (canvas_nav_) {
1011 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
1013 }
1014}
1015
1017 bool changed = false;
1019 const int normalized =
1020 std::clamp(current_map_, 0, overworld_.inspected_map_count() - 1);
1021 const int world = normalized / 0x40;
1022 changed = normalized != current_map_ || world != current_world_;
1023 current_map_ = normalized;
1024 current_world_ = world;
1025 } else {
1028 }
1029
1033 }
1034 if (const auto* map = overworld_.overworld_map(current_map_)) {
1035 current_parent_ = map->parent();
1036 } else {
1038 }
1039
1040 if (changed) {
1041 LOG_WARN("OverworldEditor",
1042 "Normalized stale overworld selection to world=%d map=%d",
1044 }
1045 return changed;
1046}
1047
1048void OverworldEditor::SelectMapForEditing(int map_id, bool respect_pin) {
1049 if (respect_pin && current_map_lock_) {
1050 return;
1051 }
1052 if (map_id < 0 || map_id >= DisplayedMapCount()) {
1053 return;
1054 }
1055 const bool project_terrain = IsProjectTerrainMap(map_id);
1056 const auto* map = overworld_.overworld_map(map_id);
1057 if (!project_terrain && !map) {
1058 return;
1059 }
1061 current_map_ = map_id;
1062 current_world_ = map_id / 0x40;
1063 current_parent_ = map->parent();
1067 if (status_.ok()) {
1068 EnsureMapTexture(map_id);
1069 }
1070 return;
1071 }
1072
1073 // Cursor tracking may cross screens during one paint stroke. Preserve its
1074 // undo batch within the same world; explicit selection still finalizes it.
1075 const bool tracking_paint = respect_pin &&
1078 map_id / 0x40 == current_world_;
1079 if (!tracking_paint) {
1081 }
1082 if (!tracking_paint || !project_terrain) {
1084 }
1085 current_map_ = map_id;
1086 current_world_ = std::clamp(map_id / 0x40, 0, 2);
1087 current_parent_ = project_terrain ? map_id : map->parent();
1088 if (project_terrain) {
1089 SetCurrentEntity(nullptr);
1091 return;
1092 }
1095
1096 if (all_gfx_loaded_) {
1098 }
1100 if (!status_.ok()) {
1102 return;
1103 }
1104
1106 if (all_gfx_loaded_) {
1109 }
1110}
1111
1112void OverworldEditor::PrimeWorldMaps(int world, bool process_texture_queue) {
1113 if (map_texture_)
1114 map_texture_->PrimeWorldMaps(world, process_texture_queue);
1115}
1116
1120
1121 const int clamped_world = std::clamp(world, 0, 2);
1122 const int local_map = current_map_ & 0x3F;
1123 const int world_start = clamped_world * 0x40;
1124 const int maps_remaining = DisplayedMapCount() - world_start;
1125 if (maps_remaining <= 0) {
1126 return;
1127 }
1128
1129 hovered_map_ = -1;
1130 SelectMapForEditing(world_start + std::min(local_map, maps_remaining - 1),
1131 false);
1134 return;
1135 }
1138 return;
1139 }
1140
1142 if (!status_.ok()) {
1144 return;
1145 }
1146
1152}
1153
1155 const zelda3::OverworldItem& item_identity) {
1156 auto* item = FindItemByIdentity(&overworld_, item_identity);
1157 if (!item) {
1158 return false;
1159 }
1160
1162 current_entity_ = item;
1164 auto* workbench = static_cast<OverworldEntityWorkbench*>(
1166 "overworld.entity_workbench"));
1167 if (workbench) {
1168 workbench->SetActiveEntity(item);
1169 }
1170 }
1171 return true;
1172}
1173
1176 current_entity_ = nullptr;
1178 auto* workbench = static_cast<OverworldEntityWorkbench*>(
1180 "overworld.entity_workbench"));
1181 if (workbench) {
1182 workbench->SetActiveEntity(nullptr);
1183 }
1184 }
1185}
1186
1188 if (is_lyra_preview()) {
1189 return nullptr;
1190 }
1192 return nullptr;
1193 return static_cast<OverworldEntityWorkbench*>(
1195 "overworld.entity_workbench"));
1196}
1197
1199 if (!selected_item_identity_.has_value()) {
1200 return nullptr;
1201 }
1202
1204 if (!item) {
1206 return nullptr;
1207 }
1208
1209 current_entity_ = item;
1210 if (auto* workbench = GetWorkbench()) {
1211 workbench->SetActiveEntity(item);
1212 }
1213 return item;
1214}
1215
1217 return const_cast<OverworldEditor*>(this)->GetSelectedItem();
1218}
1219
1226
1228 const OverworldItemsSnapshot& snapshot) {
1229 auto* items = overworld_.mutable_all_items();
1230 if (!items) {
1231 return;
1232 }
1233
1234 *items = snapshot.items;
1236 if (selected_item_identity_.has_value()) {
1237 auto* selected_item =
1239 if (selected_item) {
1240 current_entity_ = selected_item;
1241 if (auto* workbench = GetWorkbench()) {
1242 workbench->SetActiveEntity(selected_item);
1243 }
1244 } else {
1246 }
1247 } else {
1249 }
1250
1251 if (rom_) {
1252 rom_->set_dirty(true);
1253 }
1255}
1256
1258 std::string description) {
1260 if (ItemSnapshotsEqual(before, after)) {
1261 return;
1262 }
1263
1264 undo_manager_.Push(std::make_unique<OverworldItemsEditAction>(
1265 std::move(before), std::move(after),
1266 [this](const OverworldItemsSnapshot& snapshot) {
1267 RestoreItemUndoSnapshot(snapshot);
1268 },
1269 std::move(description)));
1270}
1271
1272bool OverworldEditor::DuplicateSelectedItem(int offset_x, int offset_y) {
1273 auto* selected_item = GetSelectedItem();
1274 if (!selected_item) {
1277 "Select an overworld item first (click one, or Items mode: 5)",
1279 }
1280 return false;
1281 }
1282
1283 auto before_snapshot = CaptureItemUndoSnapshot();
1286 duplicate_result = entity_mutation_service_->DuplicateItem(
1287 *selected_item, offset_x, offset_y);
1288 } else {
1289 auto duplicate_or = DuplicateItemByIdentity(&overworld_, *selected_item,
1290 offset_x, offset_y);
1291 if (duplicate_or.ok()) {
1292 duplicate_result.entity = *duplicate_or;
1293 duplicate_result.status = absl::OkStatus();
1294 } else {
1295 duplicate_result.status = duplicate_or.status();
1296 duplicate_result.error_message =
1297 "Failed to duplicate overworld item: " +
1298 std::string(duplicate_or.status().message());
1299 }
1300 }
1301 if (!duplicate_result.ok()) {
1304 duplicate_result.error_message.empty()
1305 ? "Failed to duplicate overworld item"
1306 : duplicate_result.error_message,
1308 }
1309 return false;
1310 }
1311
1312 auto* duplicated_item =
1313 static_cast<zelda3::OverworldItem*>(duplicate_result.entity);
1314 selected_item_identity_ = *duplicated_item;
1315 current_entity_ = duplicated_item;
1316 if (auto* workbench = GetWorkbench()) {
1317 workbench->SetActiveEntity(duplicated_item);
1318 }
1319 PushItemUndoAction(std::move(before_snapshot),
1320 absl::StrFormat("Duplicate overworld item 0x%02X",
1321 static_cast<int>(duplicated_item->id_)));
1322 rom_->set_dirty(true);
1325 absl::StrFormat("Duplicated item 0x%02X",
1326 static_cast<int>(duplicated_item->id_)),
1328 }
1329 return true;
1330}
1331
1332bool OverworldEditor::NudgeSelectedItem(int delta_x, int delta_y) {
1333 auto* selected_item = GetSelectedItem();
1334 if (!selected_item) {
1335 return false;
1336 }
1337
1338 auto before_snapshot = CaptureItemUndoSnapshot();
1339 auto status = NudgeItem(selected_item, delta_x, delta_y);
1340 if (!status.ok()) {
1342 dependencies_.toast_manager->Show("Failed to move selected item",
1344 }
1345 return false;
1346 }
1347
1348 selected_item_identity_ = *selected_item;
1349 current_entity_ = selected_item;
1350 if (auto* workbench = GetWorkbench()) {
1351 workbench->SetActiveEntity(selected_item);
1352 }
1354 std::move(before_snapshot),
1355 absl::StrFormat("Move overworld item 0x%02X (%+d,%+d)",
1356 static_cast<int>(selected_item->id_), delta_x, delta_y));
1357 rom_->set_dirty(true);
1358 return true;
1359}
1360
1362 auto* selected_item = GetSelectedItem();
1363 if (!selected_item) {
1364 return false;
1365 }
1366
1367 auto before_snapshot = CaptureItemUndoSnapshot();
1368 const zelda3::OverworldItem selected_identity = *selected_item;
1369 const uint8_t deleted_item_id = selected_identity.id_;
1370 absl::Status remove_status =
1371 absl::FailedPreconditionError("Entity mutation service not initialized");
1373 remove_status =
1374 entity_mutation_service_->DeleteItem(selected_identity).status;
1375 } else {
1376 remove_status = RemoveItemByIdentity(&overworld_, selected_identity);
1377 }
1378 if (!remove_status.ok()) {
1380 dependencies_.toast_manager->Show("Failed to delete selected item",
1382 }
1383 return false;
1384 }
1385
1386 auto* nearest_item =
1388 ? entity_mutation_service_->ResolveNextSelection(selected_identity)
1389 : FindNearestItemForSelection(&overworld_, selected_identity);
1390 if (nearest_item) {
1391 selected_item_identity_ = *nearest_item;
1392 current_entity_ = nearest_item;
1393 if (auto* workbench = GetWorkbench()) {
1394 workbench->SetActiveEntity(nearest_item);
1395 }
1396 } else {
1398 }
1399
1400 PushItemUndoAction(std::move(before_snapshot),
1401 absl::StrFormat("Delete overworld item 0x%02X",
1402 static_cast<int>(deleted_item_id)));
1403 rom_->set_dirty(true);
1405 if (nearest_item) {
1407 absl::StrFormat("Deleted item 0x%02X (selected nearest 0x%02X)",
1408 static_cast<int>(deleted_item_id),
1409 static_cast<int>(nearest_item->id_)),
1411 } else {
1413 absl::StrFormat("Deleted overworld item 0x%02X",
1414 static_cast<int>(deleted_item_id)),
1416 }
1417 }
1418 return true;
1419}
1420
1422 if (tile_painting_) {
1423 tile_painting_->CheckForOverworldEdits();
1424 }
1425}
1426
1428 return tile_painting_ && tile_painting_->PickTile16FromHoveredCanvas();
1429}
1430
1433 return absl::FailedPreconditionError("Clipboard unavailable");
1434 }
1435 if (tile_painting_) {
1436 if (const auto* brush = tile_painting_->selection_brush()) {
1441 return absl::OkStatus();
1442 }
1443 }
1444 // Single tile copy fallback
1445 if (current_tile16_ >= 0) {
1450 return absl::OkStatus();
1451 }
1452 return absl::FailedPreconditionError("Nothing selected to copy");
1453}
1454
1456 if (is_lyra_preview()) {
1457 return absl::FailedPreconditionError("Lyra preview is read-only");
1458 }
1460 return absl::FailedPreconditionError("Clipboard unavailable");
1461 }
1463 return absl::FailedPreconditionError("Clipboard empty");
1464 }
1465 if (!tile_painting_) {
1466 return absl::FailedPreconditionError("Tile painting is unavailable");
1467 }
1468 const auto& clipboard = *dependencies_.shared_clipboard;
1469 return tile_painting_->PasteBrush({clipboard.overworld_width,
1470 clipboard.overworld_height,
1471 clipboard.overworld_tile16_ids});
1472}
1473
1475 int map_id, const std::vector<GuardedTileCell>& cells) {
1476 if (is_lyra_preview()) {
1477 return absl::FailedPreconditionError("Lyra preview is read-only");
1478 }
1479 if (!overworld_.is_loaded() || !tile_painting_) {
1480 return absl::FailedPreconditionError("Overworld is not loaded");
1481 }
1482 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps ||
1483 IsProjectTerrainMap(map_id)) {
1484 return absl::OutOfRangeError("Stencil map must be a ROM overworld map");
1485 }
1486 SelectMapForEditing(map_id, /*respect_pin=*/false);
1487 if (current_map_ != map_id) {
1488 return absl::FailedPreconditionError("Stencil map could not be selected");
1489 }
1490 return tile_painting_->ApplyGuardedCells(map_id, cells);
1491}
1492
1494 if (canvas_nav_)
1495 return canvas_nav_->CheckForCurrentMap();
1496 return absl::OkStatus();
1497}
1498
1500 if (is_lyra_preview()) {
1501 ImGui::TextDisabled("Lyra preview: graphics editing unavailable");
1502 return absl::OkStatus();
1503 }
1504 // Delegate to the existing GfxGroupEditor
1505 if (rom_ && rom_->is_loaded()) {
1506 return gfx_group_editor_.Update();
1507 } else {
1508 gui::DrawEmptyState(gui::EmptyNoRom(/*compact=*/true));
1509 return absl::OkStatus();
1510 }
1511}
1512
1513// DrawV3Settings - now in OverworldCanvasRenderer
1514
1515// DrawMapProperties - now in OverworldCanvasRenderer
1516
1519 return absl::FailedPreconditionError(
1520 "Overworld save transaction is already active");
1521 }
1524 return absl::OkStatus();
1525}
1526
1534
1542
1544 if (graphics_source_ && graphics_source_->any_dirty())
1545 return absl::FailedPreconditionError(
1546 "Publish project graphics before Save ROM (Edit / Import Project "
1547 "Graphics)");
1548 FinishScratchEdit("Edit scratch layout");
1550 if (rom_ == nullptr) {
1551 return absl::FailedPreconditionError("No ROM for overworld sprites");
1552 }
1556 }
1559 // A disabled save domain must neither validate nor serialize staged edits.
1560 const bool saving_sprites =
1563 if (saving_sprites) {
1565 }
1566 if (!sprite_plan.empty() && dependencies_.project &&
1568 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1569 for (const auto& write : sprite_plan) {
1570 ranges.emplace_back(write.address, write.address + write.bytes.size());
1571 }
1575 "overworld sprites", "OverworldEditor", dependencies_.toast_manager));
1576 }
1577
1578 ScopedRomTransaction save_transaction(*rom_);
1579 // HACK MANIFEST VALIDATION
1580 const bool saving_maps =
1582 if (saving_maps && dependencies_.project &&
1584 const auto& manifest = dependencies_.project->hack_manifest;
1585 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1586
1587 // Calculate memory ranges that would be written by overworld map saves.
1588 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1589 // address space (LoROM), so convert before analysis.
1590 auto ranges = overworld_.GetProjectedWriteRanges();
1592 manifest, write_policy, ranges, "overworld maps", "OverworldEditor",
1594 }
1595
1596 if (saving_maps) {
1597 const auto profile = zelda3::DetectOverworldRomProfile(*rom_);
1599 if (profile.has_expanded_tile32) {
1601 } else {
1603 }
1604 if (profile.has_expanded_tile16) {
1606 } else {
1608 }
1610 }
1611 if (core::FeatureFlags::get().overworld.kSaveOverworldEntrances) {
1613 }
1614 if (core::FeatureFlags::get().overworld.kSaveOverworldExits) {
1616 }
1617 if (core::FeatureFlags::get().overworld.kSaveOverworldItems) {
1619 }
1620 if (core::FeatureFlags::get().overworld.kSaveOverworldProperties) {
1624 }
1625 if (saving_sprites) {
1627 }
1628 save_transaction.Commit();
1629 return absl::OkStatus();
1630}
1631
1632absl::Status OverworldEditor::PaintLyraA0Terrain(int map_id, int x, int y,
1633 uint16_t tile) {
1634 if (map_id != 0xA0 || !lyra_a0_terrain_writable() || x < 0 || x >= 16 ||
1635 y < 0 || y >= 16) {
1636 return absl::FailedPreconditionError("Only Lyra A0 terrain is writable");
1637 }
1638 const uint16_t before = overworld_.lyra_a0_tiles()[y * 16 + x];
1639 auto apply = [this, x, y](uint16_t value) -> absl::Status {
1641 maps_bmp_[0xA0].set_modified(true);
1642 EnsureMapTexture(0xA0);
1643 rom_->set_dirty(true);
1644 return absl::OkStatus();
1645 };
1646 // Validate even no-op requests; unsupported brushes never create history.
1648 if (before == tile) {
1649 return absl::OkStatus();
1650 }
1651 RETURN_IF_ERROR(apply(tile));
1653 std::make_unique<LyraTerrainPaintAction>(before, tile, std::move(apply)));
1654 return absl::OkStatus();
1655}
1656
1657absl::Status OverworldEditor::SaveLyraA0TerrainAs(const std::string& filename) {
1658 if (!rom_ || !lyra_a0_terrain_writable()) {
1659 return absl::FailedPreconditionError("Lyra A0 terrain writer unavailable");
1660 }
1662 const std::vector<std::pair<uint32_t, uint32_t>> ranges = {
1665 {0x14701C, 0x147020},
1666 {0x14724C, 0x147250},
1667 {0x14725C, 0x147260}};
1671 "Lyra A0 terrain", "OverworldEditor", dependencies_.toast_manager));
1672 }
1676 return absl::OkStatus();
1677}
1678
1679// ============================================================================
1680// Undo/Redo System Implementation
1681// ============================================================================
1682
1684 switch (world) {
1685 case 0:
1686 return overworld_.mutable_map_tiles()->light_world;
1687 case 1:
1688 return overworld_.mutable_map_tiles()->dark_world;
1689 default:
1690 return overworld_.mutable_map_tiles()->special_world;
1691 }
1692}
1693
1694void OverworldEditor::CreateUndoPoint(int map_id, int world, int x, int y,
1695 int old_tile_id) {
1696 auto now = std::chrono::steady_clock::now();
1697
1698 // A stroke can cross screen boundaries. Keep one batch within the same world
1699 // until release; the undo action refreshes every screen touched by its tiles.
1700 if (current_paint_operation_.has_value() &&
1701 current_paint_operation_->world == world) {
1702 // Add to existing operation
1703 current_paint_operation_->tile_changes.emplace_back(std::make_pair(x, y),
1704 old_tile_id);
1705 } else {
1706 // Finalize any pending operation before starting a new one
1708
1709 // Start new operation
1711 OverworldUndoPoint{.map_id = map_id,
1712 .world = world,
1713 .tile_changes = {{{x, y}, old_tile_id}},
1714 .timestamp = now};
1715 }
1716}
1717
1719 if (!current_paint_operation_.has_value()) {
1720 return;
1721 }
1722
1723 // Push to the UndoManager (new framework path).
1724 auto& world_tiles = GetWorldTiles(current_paint_operation_->world);
1725 std::vector<OverworldTileChange> changes;
1726 changes.reserve(current_paint_operation_->tile_changes.size());
1727 for (const auto& [coords, old_tile_id] :
1728 current_paint_operation_->tile_changes) {
1729 auto [x, y] = coords;
1730 int new_tile_id = world_tiles[x][y];
1731 changes.push_back({x, y, old_tile_id, new_tile_id});
1732 }
1733 auto action = std::make_unique<OverworldTilePaintAction>(
1735 std::move(changes), &overworld_, std::function<void()>{},
1736 [this](int map_id) {
1737 maps_bmp_[map_id].set_modified(true);
1738 RefreshOverworldMapOnDemand(map_id);
1739 },
1740 /*allow_merge=*/false);
1741 undo_manager_.Push(std::move(action));
1742
1744}
1745
1747 const OverworldPropertyEdit& edit, bool record_undo) {
1748 if (is_lyra_preview()) {
1749 return absl::FailedPreconditionError("Lyra preview is read-only");
1750 }
1752 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1753 }
1754
1755 if (!record_undo) {
1756 return map_properties_system_->ApplyPropertyEditDirect(edit);
1757 }
1758
1760 ASSIGN_OR_RETURN(const int before_value,
1761 map_properties_system_->ReadPropertyValue(edit));
1762
1763 OverworldPropertyEdit before = edit;
1764 before.value = before_value;
1765 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1766
1767 ASSIGN_OR_RETURN(const int after_value,
1768 map_properties_system_->ReadPropertyValue(edit));
1769 if (after_value == before_value) {
1770 return absl::OkStatus();
1771 }
1772
1773 OverworldPropertyEdit after = edit;
1774 after.value = after_value;
1775 undo_manager_.Push(std::make_unique<OverworldMapPropertyEditAction>(
1776 before, after,
1777 [this](const OverworldPropertyEdit& replay) {
1778 return this->ApplyOverworldPropertyEdit(replay, false);
1779 },
1781 return absl::OkStatus();
1782}
1783
1785 const std::vector<OverworldPropertyEdit>& edits,
1786 const std::string& description, bool record_undo) {
1787 if (is_lyra_preview()) {
1788 return absl::FailedPreconditionError("Lyra preview is read-only");
1789 }
1791 return absl::FailedPreconditionError("MapPropertiesSystem not initialized");
1792 }
1793 if (edits.empty()) {
1794 return absl::OkStatus();
1795 }
1796
1797 if (!record_undo) {
1798 for (const auto& edit : edits) {
1799 RETURN_IF_ERROR(map_properties_system_->ApplyPropertyEditDirect(edit));
1800 }
1801 return absl::OkStatus();
1802 }
1803
1805
1806 std::vector<OverworldPropertyEdit> before_edits;
1807 std::vector<OverworldPropertyEdit> after_edits;
1808 before_edits.reserve(edits.size());
1809 after_edits.reserve(edits.size());
1810
1811 for (const auto& edit : edits) {
1812 if (!map_properties_system_->CheckPropertyEditSupported(edit).ok()) {
1813 continue;
1814 }
1815
1816 auto before_value = map_properties_system_->ReadPropertyValue(edit);
1817 if (!before_value.ok()) {
1818 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1819 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1820 }
1821 return before_value.status();
1822 }
1823
1824 OverworldPropertyEdit before = edit;
1825 before.value = *before_value;
1826 const absl::Status apply_status =
1827 map_properties_system_->ApplyPropertyEditDirect(edit);
1828 if (!apply_status.ok()) {
1829 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1830 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1831 }
1832 return apply_status;
1833 }
1834
1835 auto after_value = map_properties_system_->ReadPropertyValue(edit);
1836 if (!after_value.ok()) {
1837 (void)map_properties_system_->ApplyPropertyEditDirect(before);
1838 for (auto it = before_edits.rbegin(); it != before_edits.rend(); ++it) {
1839 (void)map_properties_system_->ApplyPropertyEditDirect(*it);
1840 }
1841 return after_value.status();
1842 }
1843 if (*after_value == *before_value) {
1844 continue;
1845 }
1846
1847 OverworldPropertyEdit after = edit;
1848 after.value = *after_value;
1849 before_edits.push_back(std::move(before));
1850 after_edits.push_back(std::move(after));
1851 }
1852
1853 if (after_edits.empty()) {
1854 return absl::OkStatus();
1855 }
1856
1857 const std::string action_description =
1858 description.empty()
1859 ? absl::StrFormat("Edit %d overworld map properties",
1860 static_cast<int>(after_edits.size()))
1861 : description;
1862 undo_manager_.Push(std::make_unique<OverworldMapPropertyBatchEditAction>(
1863 before_edits, after_edits,
1864 [this](const OverworldPropertyEdit& replay) {
1865 return this->ApplyOverworldPropertyEdit(replay, false);
1866 },
1867 action_description));
1868 return absl::OkStatus();
1869}
1870
1872 const std::string& type, int id, const std::string& label) {
1873 if (is_lyra_preview()) {
1874 return absl::FailedPreconditionError("Lyra preview is read-only");
1875 }
1876 if (!dependencies_.project) {
1877 return absl::FailedPreconditionError("No project is open");
1878 }
1879
1881 const std::string before =
1885 const std::string after =
1887 if (after == before) {
1888 return absl::OkStatus();
1889 }
1890
1891 undo_manager_.Push(std::make_unique<OverworldProjectLabelEditAction>(
1892 before, after,
1893 [this, type, id](const std::string& value) {
1895 value);
1896 },
1897 absl::StrFormat("Rename %s 0x%02X", type, id)));
1898 return absl::OkStatus();
1899}
1900
1902 FinishScratchEdit("Edit scratch layout");
1905 return absl::FailedPreconditionError("Lyra preview is read-only");
1906 }
1907 // Finalize any pending paint operation first
1909 return undo_manager_.Undo();
1910}
1911
1913 FinishScratchEdit("Edit scratch layout");
1916 return absl::FailedPreconditionError("Lyra preview is read-only");
1917 }
1919 return undo_manager_.Redo();
1920}
1921
1922// ============================================================================
1923
1925 const std::string path = ProjectGraphicsPath();
1926 if (!force && path == graphics_preview_config_path_)
1927 return absl::OkStatus();
1928 if (path != graphics_preview_config_path_) {
1931 }
1933 graphics_preview_baseline_ = std::move(preview);
1936}
1937
1939 gfx::ScopedTimer timer("LoadGraphics");
1940
1941 LOG_DEBUG("OverworldEditor", "Loading overworld.");
1943 // The model can inspect legacy overlays, but the read-only canvas does not
1944 // yet qualify their final compositing for this descriptor format.
1945 show_overlay_preview_ = false;
1948 current_map_ = 0xA0;
1949 current_world_ = 2;
1955 if (map_texture_) {
1956 map_texture_->ResetMapBitmaps();
1957 }
1958 return absl::OkStatus();
1959 }
1961 // Load the Link to the Past overworld.
1962 {
1963 gfx::ScopedTimer load_timer("Overworld::Load");
1966 }
1968
1969 // Fix: Set transparency for the first color of each 16-color subpalette
1970 // This ensures the background color (backdrop) shows through
1971 for (size_t i = 0; i < palette_.size(); i += 16) {
1972 if (i < palette_.size()) {
1973 palette_[i].set_transparent(true);
1974 }
1975 }
1976
1977 LOG_DEBUG("OverworldEditor", "Loading overworld graphics (optimized).");
1978
1979 // Phase 1: Create bitmaps without textures for faster loading
1980 // This avoids blocking the main thread with GPU texture creation
1981 {
1982 gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
1988 }
1989
1990 LOG_DEBUG("OverworldEditor",
1991 "Loading overworld tileset (deferred textures).");
1992 {
1993 gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
1994 tile16_blockset_bmp_.Create(0x80, 0x2000, 0x08,
1999 }
2000 map_blockset_loaded_ = true;
2001
2002 // Copy the tile16 data into individual tiles.
2003 const auto& tile16_blockset_data = overworld_.tile16_blockset_data();
2004 LOG_DEBUG("OverworldEditor", "Loading overworld tile16 graphics.");
2005
2006 {
2007 gfx::ScopedTimer tilemap_timer("CreateTilemap");
2009 gfx::CreateTilemap(renderer_, tile16_blockset_data, 0x80, 0x2000,
2011
2012 // Queue texture creation for the tile16 blockset atlas
2017 }
2018 }
2019
2020 // Phase 2: reset map bitmaps and leave them fully on-demand.
2021 // The canvas/navigation path will materialize only the visible/current maps.
2022 if (map_texture_) {
2023 map_texture_->ResetMapBitmaps();
2024 }
2025
2026 if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
2027 {
2028 gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
2030 }
2031 }
2032
2033 return absl::OkStatus();
2034}
2035
2037 // Render the sprites for each Overworld map
2038 const int depth = 0x10;
2039 for (int i = 0; i < 3; i++)
2040 for (auto const& sprite : *overworld_.mutable_sprites(i)) {
2041 int width = sprite.width();
2042 int height = sprite.height();
2043 if (width == 0 || height == 0) {
2044 continue;
2045 }
2046 if (sprite_previews_.size() < sprite.id()) {
2047 sprite_previews_.resize(sprite.id() + 1);
2048 }
2049 sprite_previews_[sprite.id()].Create(width, height, depth,
2050 *sprite.preview_graphics());
2051 sprite_previews_[sprite.id()].SetPalette(palette_);
2054 &sprite_previews_[sprite.id()]);
2055 }
2056 return absl::OkStatus();
2057}
2058
2060 if (map_texture_)
2061 map_texture_->ProcessDeferredTextures();
2062}
2063
2065 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
2066 return nullptr;
2067 }
2068 EnsureMapTexture(map_id);
2069 const gfx::Bitmap& bitmap = maps_bmp_[map_id];
2070 return bitmap.is_active() && bitmap.texture() ? &bitmap : nullptr;
2071}
2072
2074 if (map_texture_ && !IsProjectTerrainMap(map_index))
2075 map_texture_->EnsureMapTexture(map_index);
2076}
2077
2080 ctx.overworld = &overworld_;
2081 ctx.maps_bmp = &maps_bmp_;
2082 ctx.renderer = renderer_;
2085 ctx.refresh_map_on_demand = [this](int map_index) {
2086 this->RefreshOverworldMapOnDemand(map_index);
2087 };
2088 map_texture_ = std::make_unique<OverworldMapTextureCoordinator>(ctx);
2089}
2090
2093 ctx.rom = rom_;
2094 ctx.overworld = &overworld_;
2095 ctx.maps_bmp = &maps_bmp_;
2099 ctx.palette = &palette_;
2100 ctx.renderer = renderer_;
2105 ctx.game_state = &game_state_;
2107 ctx.status = &status_;
2108 ctx.ensure_map_texture = [this](int map_index) {
2109 this->EnsureMapTexture(map_index);
2110 };
2111 map_refresh_ = std::make_unique<MapRefreshCoordinator>(ctx);
2112}
2113
2115 if (canvas_nav_)
2116 canvas_nav_->HandleMapInteraction();
2117}
2118
2120 if (canvas_nav_)
2121 canvas_nav_->ScrollBlocksetCanvasToCurrentTile();
2122}
2123
2125 int quadrant) {
2126 if (rom_ == nullptr || !rom_->is_loaded() || game_data_ == nullptr ||
2127 !overworld_.is_loaded()) {
2128 return absl::FailedPreconditionError("Overworld ROM session is not loaded");
2129 }
2130 if (tile16_id < 0 ||
2131 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
2132 return absl::InvalidArgumentError("Selected Tile16 is unavailable");
2133 }
2134 auto* manager = static_cast<EditorManager*>(dependencies_.custom_data);
2135 if (manager == nullptr) {
2136 return absl::FailedPreconditionError("Graphics editor is unavailable");
2137 }
2139 const int source_map_id = current_map_;
2142 const auto* map = overworld_.overworld_map(source_map_id);
2143 if (map == nullptr || !map->is_built()) {
2144 return absl::FailedPreconditionError("Selected overworld map is not built");
2145 }
2146 if (map->HasGraphicsPreview()) {
2147 return absl::UnimplementedError(
2148 "Project graphics overrides need an explicit editable source map");
2149 }
2151 *map, overworld_.tiles16()[tile16_id], tile16_id, quadrant, game_data_);
2152 if (!context.ok()) {
2153 return context.status();
2154 }
2155 // Keep the source map's resolved assignments, palette, parent, and special
2156 // area state. The copied renderer reads live tile/map definitions and every
2157 // watched sheet from this ROM session on each preview refresh.
2158 auto preview_map = std::make_shared<zelda3::OverworldMap>(*map);
2159 const auto* owner = game_data_;
2160 context->resolve_live_source = [this, owner, source_map_id, tile16_id,
2161 quadrant](GraphicsEditContext& fresh) {
2162 if (game_data_ != owner || !overworld_.is_loaded() ||
2163 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
2164 return absl::FailedPreconditionError("Overworld source session changed");
2165 }
2167 overworld_.SyncAreaProperties(source_map_id);
2168 auto* live = overworld_.mutable_overworld_map(source_map_id);
2169 if (!live || live->HasGraphicsPreview()) {
2170 return absl::UnimplementedError(
2171 "Project graphics overrides have no writable source");
2172 }
2173 live->LoadAreaGraphics();
2174 RETURN_IF_ERROR(live->LoadPalette());
2175 auto resolved = MakeOverworldGraphicsEditContext(
2176 *live, overworld_.tiles16()[tile16_id], tile16_id, quadrant, owner);
2177 if (!resolved.ok())
2178 return resolved.status();
2179 fresh = std::move(*resolved);
2180 return absl::OkStatus();
2181 };
2182 context->limitation += " Palette colors follow live edits.";
2183 context->refresh_palettes = [this, preview_map, owner, tile16_id, quadrant](
2184 GraphicsEditContext& source) -> absl::Status {
2185 if (game_data_ != owner || !overworld_.is_loaded() ||
2186 tile16_id >= static_cast<int>(overworld_.tiles16().size())) {
2187 return absl::FailedPreconditionError("Overworld source session changed");
2188 }
2189 RETURN_IF_ERROR(preview_map->LoadPalette());
2190 auto refreshed = MakeOverworldGraphicsEditContext(
2191 *preview_map, overworld_.tiles16()[tile16_id], tile16_id, quadrant,
2192 owner);
2193 if (!refreshed.ok())
2194 return refreshed.status();
2195 return UpdateGraphicsContextPalettes(source, std::move(*refreshed));
2196 };
2197 context->preview_revision = [this, owner,
2198 source_map_id]() -> absl::StatusOr<uint64_t> {
2199 if (game_data_ != owner || !overworld_.is_loaded())
2200 return absl::FailedPreconditionError("Overworld source session changed");
2201 constexpr uint64_t kPrime = 1099511628211ULL;
2202 uint64_t hash = 1469598103934665603ULL;
2203 auto add = [&](uint64_t value) {
2204 hash = (hash ^ value) * kPrime;
2205 };
2206 for (const auto& tile : overworld_.tiles16())
2207 for (const auto& quadrant : tile.tiles_info)
2208 add(gfx::TileInfoToWord(quadrant));
2209 const auto& placements =
2211 const int local =
2212 source_map_id - zelda3::WorldForOverworldMap(source_map_id) * 0x40;
2213 const int x0 = (local % 8) * 32;
2214 const int y0 = (local / 8) * 32;
2215 for (int x = x0; x < x0 + 32; ++x) {
2216 if (x >= static_cast<int>(placements.size()) ||
2217 y0 + 32 > static_cast<int>(placements[x].size()))
2218 return absl::FailedPreconditionError("Overworld source grid changed");
2219 for (int y = y0; y < y0 + 32; ++y)
2220 add(placements[x][y]);
2221 }
2222 return hash;
2223 };
2224 context->render_preview =
2225 [this, preview_map, owner,
2226 source_map_id]() -> absl::StatusOr<usage_preview::PreviewImage> {
2227 if (game_data_ != owner || !overworld_.is_loaded()) {
2228 return absl::FailedPreconditionError(
2229 "Overworld graphics source session has changed");
2230 }
2231 std::set<uint16_t> ids;
2232 for (int slot = 0; slot < 8; ++slot) {
2233 ids.insert(preview_map->static_graphics(slot));
2234 }
2235 ids.insert(preview_map->animated_sheet());
2236 preview_map->SetGraphicsSheetOverrides(
2238 RETURN_IF_ERROR(preview_map->BuildTileset());
2239 RETURN_IF_ERROR(preview_map->BuildTiles16Gfx(
2241 static_cast<int>(overworld_.tiles16().size())));
2242 const int world = zelda3::WorldForOverworldMap(source_map_id);
2243 RETURN_IF_ERROR(preview_map->BuildBitmap(overworld_.GetMapTiles(world)));
2245 image.width = 512;
2246 image.height = 512;
2247 image.pixels = preview_map->bitmap_data();
2248 image.index0_transparent = false;
2249 const auto& palette = preview_map->current_palette();
2250 for (size_t i = 0; i < palette.size() && i < image.colors.size(); ++i) {
2251 const auto rgb = palette[i].rgb();
2252 image.colors[i] =
2253 SDL_Color{static_cast<uint8_t>(rgb.x), static_cast<uint8_t>(rgb.y),
2254 static_cast<uint8_t>(rgb.z), 255};
2255 }
2256 return image;
2257 };
2258 context->return_to_source = [this, manager, source_map_id, tile16_id]() {
2259 manager->SwitchToEditor(EditorType::kOverworld, true);
2260 SelectMapForEditing(source_map_id, false);
2261 RequestTile16Selection(tile16_id);
2265 }
2266 };
2267 auto* editor_set = manager->GetCurrentEditorSet();
2268 if (editor_set == nullptr || editor_set->GetGraphicsEditor() == nullptr) {
2269 return absl::FailedPreconditionError("Graphics editor is unavailable");
2270 }
2271 RETURN_IF_ERROR(manager->EnsureEditorAssetsLoaded(EditorType::kGraphics));
2273 editor_set->GetGraphicsEditor()->OpenInContext(std::move(*context)));
2274 manager->SwitchToEditor(EditorType::kGraphics, true);
2275 return absl::OkStatus();
2276}
2277
2279 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
2280 return;
2281 }
2282
2283 if (!rom_ || !rom_->is_loaded() || !all_gfx_loaded_ ||
2285 current_tile16_ = tile_id;
2287 return;
2288 }
2289
2291
2292 if (tile_id == tile16_editor_.current_tile16()) {
2293 current_tile16_ = tile_id;
2294 auto status = tile16_editor_.SetCurrentTile(tile_id);
2295 if (!status.ok()) {
2296 LOG_WARN("OverworldEditor", "Failed to refresh selected Tile16 %d: %s",
2297 tile_id, status.message().data());
2298 }
2299 return;
2300 }
2301
2304}
2305
2307 graphics_source_.reset();
2308 graphics_source_project_ = nullptr;
2309 graphics_source_open_ = false;
2311 graphics_import_asset_.reset();
2312 graphics_import_plan_.reset();
2313 graphics_import_pack_.clear();
2318 lyra_loaded_rom_sha1_.clear();
2319 if (is_lyra_preview()) {
2320 show_overlay_preview_ = true;
2321 }
2322
2323 // Unregister palette listener
2324 if (palette_listener_id_ >= 0) {
2327 }
2328
2330 current_graphics_set_.clear();
2331 all_gfx_loaded_ = false;
2332 map_blockset_loaded_ = false;
2333 return absl::OkStatus();
2334}
2335
2337 if (canvas_nav_)
2338 canvas_nav_->UpdateBlocksetSelectorState();
2339}
2340
2342 const int next =
2343 std::clamp(current_tile16_ + delta, 0, zelda3::kNumTile16Individual - 1);
2345}
2346
2350
2354
2363
2372
2374 const int world = std::clamp(current_world_, 0, 2);
2375 const int local = current_map_ - world * 0x40;
2376 const int rows = std::min(64, DisplayedMapCount() - world * 0x40) / 8;
2377 const int x = local % 8 + dx;
2378 const int y = local / 8 + dy;
2379 if (x < 0 || x >= 8 || y < 0 || y >= rows) {
2380 return;
2381 }
2382 const int target = world * 0x40 + y * 8 + x;
2383 if (target >= DisplayedMapCount()) {
2384 return;
2385 }
2386 SelectMapForEditing(target, /*respect_pin=*/false);
2387 if (canvas_nav_) {
2388 // Keep the keyboard choice until the mouse moves, then resume following
2389 // the cursor (unless pinned).
2390 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
2391 canvas_nav_->CenterOnMap(target);
2392 }
2393}
2394
2396 if (map_id < 0 || map_id >= DisplayedMapCount()) {
2397 return;
2398 }
2399 if (!overworld_.is_loaded()) {
2400 set_current_map(map_id);
2401 return;
2402 }
2403 const int world = std::clamp(map_id / 0x40, 0, 2);
2404 if (world != current_world_) {
2405 SwitchToWorld(world);
2406 }
2407 SelectMapForEditing(map_id, /*respect_pin=*/false);
2408 if (canvas_nav_) {
2409 // Same as keyboard navigation: keep the choice until the mouse moves.
2410 canvas_nav_->SuspendHoverFollowUntilMouseMoves();
2411 canvas_nav_->CenterOnMap(map_id);
2412 }
2413}
2414
2415void OverworldEditor::LocateOverworldTile(int map_id, int world_x,
2416 int world_y) {
2417 JumpToMap(map_id);
2418 if (canvas_nav_) {
2419 canvas_nav_->CenterOnWorldTile(world_x, world_y);
2420 }
2421}
2422
2436
2439 return;
2440 }
2441 entity_edit_mode_ = mode;
2442 if (mode != EntityEditMode::NONE) {
2443 // Entity work happens with the select tool.
2446 }
2447}
2448
2450 auto config = ow_map_canvas_.GetConfig();
2451 config.enable_grid = !config.enable_grid;
2453}
2454
2458
2460 auto* window_manager = dependencies_.window_manager;
2461 if (!window_manager) {
2462 return;
2463 }
2464 const size_t session_id = window_manager->GetActiveSessionId();
2465 if (window_manager->IsWindowOpen(session_id,
2467 window_manager->CloseWindow(session_id, OverworldPanelIds::kMapProperties);
2468 } else {
2469 window_manager->OpenWindowFloating(session_id,
2471 window_manager->MarkWindowRecentlyUsed(OverworldPanelIds::kMapProperties);
2472 const std::string window_name = window_manager->GetWorkspaceWindowName(
2474 if (!window_name.empty()) {
2475 ImGui::SetWindowFocus(window_name.c_str());
2476 }
2477 }
2478}
2479
2481 auto* window_manager = dependencies_.window_manager;
2482 const size_t session_id =
2483 window_manager ? window_manager->GetActiveSessionId() : 0;
2484
2485 const bool properties_open =
2486 window_manager && window_manager->IsWindowOpen(
2488 if (ImGui::MenuItem("Map Properties...", "M", properties_open)) {
2490 }
2491
2492 ImGui::Separator();
2493
2494 if (ImGui::BeginMenu("Scratch Workspace")) {
2495 const auto& pages = scratch_workspace_.state().pages;
2496 const auto& page = scratch_workspace_.current();
2497 int current_page_idx = 0;
2498 for (size_t i = 0; i < pages.size(); ++i) {
2499 if (pages[i].id == scratch_workspace_.state().selected_id) {
2500 current_page_idx = static_cast<int>(i);
2501 break;
2502 }
2503 }
2504 if (ImGui::MenuItem("Previous Layout", nullptr, false,
2505 current_page_idx > 0)) {
2506 FinishScratchEdit("Switch scratch layout");
2507 scratch_workspace_.Select(pages[current_page_idx - 1].id);
2510 }
2511 if (ImGui::MenuItem(
2512 "Next Layout", nullptr, false,
2513 current_page_idx < static_cast<int>(pages.size()) - 1)) {
2514 FinishScratchEdit("Switch scratch layout");
2515 scratch_workspace_.Select(pages[current_page_idx + 1].id);
2518 }
2519 if (ImGui::MenuItem("New Layout", nullptr, false,
2520 pages.size() < ScratchWorkspace::kMaxPages)) {
2523 FinishScratchEdit("New scratch layout");
2525 }
2526 if (ImGui::MenuItem("Duplicate Layout", nullptr, false,
2527 pages.size() < ScratchWorkspace::kMaxPages)) {
2529 (void)scratch_workspace_.AddPage(true);
2530 FinishScratchEdit("Duplicate scratch layout");
2532 }
2533 if (ImGui::MenuItem("Clear Layout")) {
2534 (void)ClearScratchSpace();
2535 }
2536 if (ImGui::MenuItem("Delete Layout", nullptr, false, pages.size() > 1)) {
2539 FinishScratchEdit("Delete scratch layout");
2541 }
2542 ImGui::Separator();
2543 const auto* active_brush = selection_brush();
2544 const bool can_capture = (active_brush && active_brush->valid());
2545 if (ImGui::MenuItem(
2546 "Capture Selection to Scratch", nullptr, false,
2547 can_capture && pages.size() < ScratchWorkspace::kMaxPages)) {
2549 }
2550 if (ImGui::MenuItem("Copy Layout as Brush", nullptr, false, page.in_use)) {
2551 (void)LoadScratchToSelection();
2552 }
2553 if (ImGui::MenuItem("Refresh Preview")) {
2555 }
2556 ImGui::EndMenu();
2557 }
2558
2559 ImGui::Separator();
2560
2561 if (ImGui::BeginMenu("Entities")) {
2562 ImGui::MenuItem("Show Entities", nullptr, &show_entities_);
2563 ImGui::Separator();
2564 if (ImGui::MenuItem("All Entities (Select Mode)", nullptr,
2567 }
2568 if (ImGui::MenuItem("Entrances Mode", nullptr,
2571 }
2572 if (ImGui::MenuItem("Exits Mode", nullptr,
2575 }
2576 if (ImGui::MenuItem("Items Mode", nullptr,
2579 }
2580 if (ImGui::MenuItem("Sprites Mode", nullptr,
2583 }
2584 ImGui::EndMenu();
2585 }
2586
2587 if (ImGui::BeginMenu("Tiles & Graphics")) {
2588 if (ImGui::MenuItem("Tile16 Editor", nullptr, false)) {
2590 }
2591 if (ImGui::MenuItem("Item List", nullptr, false)) {
2593 }
2594 ImGui::EndMenu();
2595 }
2596
2597 if (ImGui::BeginMenu("Tile16", Tile16MenuEnabled())) {
2599 ImGui::EndMenu();
2600 }
2601
2602 ImGui::Separator();
2603
2604 if (ImGui::BeginMenu("Diagnostics")) {
2605 if (ImGui::MenuItem("Usage Statistics", nullptr, false)) {
2606 if (window_manager) {
2607 window_manager->OpenWindow(session_id, OverworldPanelIds::kUsageStats);
2608 }
2609 }
2610 if (ImGui::MenuItem("Debug Window", nullptr, false)) {
2611 if (window_manager) {
2612 window_manager->OpenWindow(session_id, OverworldPanelIds::kDebugWindow);
2613 }
2614 }
2615 ImGui::EndMenu();
2616 }
2617}
2618
2620 ImGui::Separator();
2621 ImGui::BeginDisabled(!Tile16MenuEnabled());
2623 ImGui::EndDisabled();
2624}
2625
2626// Tile16 menu actions act on the Tile16 editor's current tile, so offer them
2627// only while that window is open and its blockset is loaded.
2629 auto* window_manager = dependencies_.window_manager;
2630 return window_manager && tile16_editor_.blockset_loaded() &&
2631 window_manager->IsWindowOpen(window_manager->GetActiveSessionId(),
2633}
2634
2636 if (!status_bar)
2637 return;
2638 status_bar->SetCustomSegment(
2639 "Map",
2643 status_bar->SetCustomSegment("Tile16",
2644 absl::StrFormat("0x%03X", current_tile16_));
2646 const auto mouse = ow_map_canvas_.hover_mouse_pos();
2647 const float scale = ow_map_canvas_.global_scale() > 0
2649 : 1.0f;
2650 const int tx = static_cast<int>(mouse.x / (16.0f * scale)) % 32;
2651 const int ty = static_cast<int>(mouse.y / (16.0f * scale)) % 32;
2652 status_bar->SetCustomSegment("Pos", absl::StrFormat("(%d, %d)", tx, ty));
2653 } else {
2654 status_bar->ClearCustomSegment("Pos");
2655 }
2656
2657 const char* mode_label;
2658 switch (entity_edit_mode_) {
2660 mode_label = "Entrances";
2661 break;
2663 mode_label = "Exits";
2664 break;
2666 mode_label = "Items";
2667 break;
2669 mode_label = "Sprites";
2670 break;
2672 mode_label = "Transports";
2673 break;
2675 mode_label = "Music";
2676 break;
2678 switch (current_mode) {
2679 case EditingMode::MOUSE:
2680 mode_label = "Mouse";
2681 break;
2683 mode_label = "Draw";
2684 break;
2686 mode_label = "Fill";
2687 break;
2688 default:
2689 mode_label = "Draw";
2690 break;
2691 }
2692 break;
2693 default:
2694 mode_label = "Draw";
2695 break;
2696 }
2697 status_bar->SetEditorMode(mode_label);
2698}
2699
2701 EditorContextSnapshot snapshot;
2702 snapshot.category = "Overworld";
2703 snapshot.semantic_owner = absl::StrFormat("overworld.map.%03X", current_map_);
2704
2705 if (!overworld_.is_loaded() || current_map_ < 0) {
2706 snapshot.title = "Overworld";
2707 snapshot.subtitle = "Load a ROM to inspect map context";
2708 return snapshot;
2709 }
2710
2713 snapshot.title = map.map_title;
2714 snapshot.subtitle =
2715 absl::StrFormat("%s · %s", map.world_label, map.area_size_label);
2716 snapshot.metadata = {
2717 {.id = "map", .label = "Map", .value = map.map_id_label},
2718 {.id = "parent", .label = "Parent", .value = map.parent_label},
2719 {.id = "tile16",
2720 .label = "Tile16",
2721 .value = absl::StrFormat("0x%03X", current_tile16_)},
2722 {.id = "graphics", .label = "Graphics", .value = map.area_gfx_label},
2723 {.id = "palette", .label = "Palette", .value = map.area_palette_label},
2724 };
2725
2726 const int owner_map = current_parent_;
2727 const auto belongs_to_area = [owner_map](const auto& entity) {
2728 return static_cast<int>(entity.map_id_) == owner_map;
2729 };
2730 const auto sprites = overworld_.sprites(game_state_);
2731 const auto items = overworld_.all_items();
2732 const auto* exits = overworld_.exits();
2733 snapshot.counts = {
2734 {.id = "sprites",
2735 .label = "Sprites",
2736 .value = std::to_string(std::count_if(
2737 sprites.begin(), sprites.end(),
2738 [owner_map](const zelda3::Sprite& sprite) {
2739 return static_cast<int>(sprite.map_id()) == owner_map;
2740 }))},
2741 {.id = "items",
2742 .label = "Items",
2743 .value = std::to_string(
2744 std::count_if(items.begin(), items.end(), belongs_to_area))},
2745 {.id = "entrances",
2746 .label = "Entrances",
2747 .value = std::to_string(std::count_if(overworld_.entrances().begin(),
2748 overworld_.entrances().end(),
2749 belongs_to_area))},
2750 {.id = "exits",
2751 .label = "Exits",
2752 .value = exits ? std::to_string(std::count_if(
2753 exits->begin(), exits->end(), belongs_to_area))
2754 : "0"},
2755 };
2756
2757 snapshot.has_pending_changes = rom_ != nullptr && rom_->dirty();
2758 snapshot.pending_label =
2759 snapshot.has_pending_changes ? "ROM buffer has pending changes" : "";
2760 if (current_map_lock_) {
2761 snapshot.diagnostics.push_back({
2762 .id = "map_pinned",
2764 .message = "Map context is pinned; cursor hover will not replace it.",
2765 });
2766 }
2767 if (!selected_tile16_ids_.empty()) {
2768 snapshot.diagnostics.push_back({
2769 .id = "tile_selection",
2771 .message = absl::StrFormat("%zu Tile16 cells selected",
2772 selected_tile16_ids_.size()),
2773 });
2774 }
2775
2776 snapshot.capabilities = {
2777 "overworld.properties",
2778 "overworld.entities",
2779 "overworld.tile16",
2780 };
2781 snapshot.actions = {
2782 {.id = "open_properties",
2783 .label = "Map Properties",
2784 .target = "overworld.properties"},
2785 {.id = "open_entities",
2786 .label = "Entity Workbench",
2787 .target = "overworld.entity_workbench"},
2788 {.id = "open_tile16",
2789 .label = "Tile16 Selector",
2790 .target = "overworld.tile16_selector"},
2791 };
2792 if (const auto* map = overworld_.overworld_map(current_map_)) {
2794 if (dependencies_.project != nullptr) {
2796 }
2798 snapshot, zelda3::DescribeOverworldGraphicsContext(*map, identity));
2799 }
2800 return snapshot;
2801}
2802
2814
2816 if (tile_painting_) {
2817 tile_painting_->ClearSelectionBrush();
2818 }
2819}
2820
2822 return tile_painting_ ? tile_painting_->selection_brush() : nullptr;
2823}
2824
2826 return canvas_renderer_ ? canvas_renderer_->tile16_usage_count(tile_id) : 0;
2827}
2828
2830 return canvas_renderer_ ? canvas_renderer_->Tile16ScopeLabel()
2831 : std::string();
2832}
2833
2834} // 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:52
absl::Status CheckLayoutWritable() const
Definition rom.cc:432
absl::Status AttachLyraPrivateReservation(const core::HackManifest &manifest)
Definition rom.cc:518
rom::LyraLayoutInspection InspectLyraLayout() const
Definition rom.cc:421
void set_dirty(bool dirty)
Definition rom.h:201
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool dirty() const
Definition rom.h:200
absl::Status SaveLyraA0TerrainAs(const std::array< uint16_t, 256 > &tiles, const std::string &filename)
Definition rom.cc:842
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
bool loaded() const
Check if the manifest has been loaded.
absl::StatusOr< uint16_t > tile(int global_x, int global_y) const
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:376
UndoManager undo_manager_
Definition editor.h:383
project::YazeProject * project() const
Definition editor.h:370
EditorDependencies dependencies_
Definition editor.h:382
EditorType type() const
Definition editor.h:355
void SetHostSurfaceHint(std::string hint)
void DrawProperties(int map_id, int &state, zelda3::Overworld &overworld, UndoManager &history)
absl::Status PaintPattern(const TileBrush &brush, int x, int y, int width, int height, bool finalize, UndoManager &history)
absl::Status SaveAs(project::YazeProject *project, const std::string &project_path, zelda3::Overworld &overworld)
gfx::Bitmap & MapAtlas(int map_id)
absl::Status Create(project::YazeProject *project, zelda3::Overworld &overworld)
const core::LyraTerrainDocument * document() const
absl::Status Load(project::YazeProject *project, zelda3::Overworld &overworld)
absl::Status Save(project::YazeProject *project, zelda3::Overworld &overworld)
void FinishStroke(UndoManager &history)
absl::Status Open(project::YazeProject *project, const std::string &path, zelda3::Overworld &overworld)
Main UI class for editing overworld maps in A Link to the Past.
bool IsProjectTerrainMap(int map_id) const
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_
absl::Status SaveProjectTerrainAs(project::YazeProject *project, const std::string &project_path)
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)
absl::StatusOr< int > ApplyTile16Stencil(int map_id, const std::vector< GuardedTileCell > &cells)
Select map_id and apply a guarded stencil as one undoable paint.
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)
zelda3::OverworldGraphicsPreview graphics_preview_baseline_
void InitMapRefreshCoordinator()
Initialize the map refresh coordinator (called during Initialize)
void RequestItemInsertPopup(const std::optional< OverworldContextTarget > &target)
std::optional< zelda3::OverworldItem > selected_item_identity_
void SetSelectionBrush(TileBrush brush)
absl::Status RefreshMapPalette(int map_id=-1)
absl::Status PaintLyraA0Terrain(int map_id, int x, int y, uint16_t tile)
std::unique_ptr< zelda3::OverworldUpgradeSystem > upgrade_system_
void RequestJumpToEntrance(int entrance_id)
Trigger a jump to a dungeon entrance.
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
absl::Status OpenLyraTerrain(const std::string &path)
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_
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_
int GetTile16UsageCount(int tile_id) const
const TileBrush * selection_brush() const
void FinishScratchEdit(const std::string &description)
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...
static bool NormalizeMapSelection(int &current_world, int &current_map, int map_count=zelda3::kNumOverworldMaps)
Clamp a possibly stale world/map selection back into a valid overworld range.
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)
absl::Status SaveCurrentSelectionToScratch()
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::string GetTile16ScopeLabel() const
std::unique_ptr< OverworldEntityRenderer > entity_renderer_
std::optional< OverworldEntityInsertionRequest > pending_entity_insertion_
absl::Status Load() override
absl::Status RebuildScratchBitmapFromTileData()
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::shared_ptr< ProjectGraphicsSource > graphics_source_
std::unique_ptr< CanvasNavigationManager > canvas_nav_
absl::Status SaveLyraA0TerrainAs(const std::string &filename)
bool NudgeSelectedItem(int delta_x, int delta_y)
Move selected item by signed pixel deltas.
const project::YazeProject * graphics_source_project_
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_
project::YazeProject * TerrainProject() const
void RefreshMapProperties(int map_id=-1)
absl::Status Paste() override
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset canvas to show the current selected tile16.
std::optional< ProjectGraphicsImportPlan > graphics_import_plan_
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::optional< ProjectGraphicsImportAsset > graphics_import_asset_
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)
const ScratchWorkspaceState & state() const
static constexpr size_t kMaxPages
absl::Status AddPage(bool duplicate=false)
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.
void ClearCustomSegment(const std::string &key)
Remove a custom segment.
void SetCollisionTypes(const std::array< uint8_t, 0x200 > &types)
absl::Status SetCurrentTile(int id)
void SetHoverDetailProvider(std::function< std::string(int)> provider)
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)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
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.
LyraTerrainPaintAction(uint16_t before, uint16_t after, std::function< absl::Status(uint16_t)> apply)
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
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:386
auto hover_mouse_pos() const
Definition canvas.h:476
void ApplyConfigSnapshot(const CanvasConfig &snapshot)
Definition canvas.cc:1039
void ClearContextMenuItems()
Definition canvas.cc:995
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:845
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:158
void SyncAreaProperties(int map_index)
Copy the area parent's render settings (main palette, animated GFX, tile GFX groups,...
absl::Status SetLyraA0Tile32(int x, int y, uint16_t tile)
Definition overworld.cc:359
const std::vector< uint8_t > & current_graphics() const
Definition overworld.h:797
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 ValidatePrivateNativeTerrainSave() const
absl::Status SaveMap32Tiles()
Save tile32 definitions to ROM.
const gfx::SnesPalette & current_area_palette() const
Definition overworld.h:813
absl::Status SaveMap16Tiles()
Save tile16 definitions to ROM.
auto sprites(int state) const
Definition overworld.h:787
auto all_items() const
Definition overworld.h:868
const std::vector< gfx::Tile16 > & tiles16() const
Definition overworld.h:784
bool is_lyra_preview() const
Definition overworld.h:444
auto is_loaded() const
Definition overworld.h:836
int inspected_map_count() const
Definition overworld.h:447
absl::Status CreateTile32Tilemap()
Build tile32 tilemap from current tile16 data.
auto overworld_map(int i) const
Definition overworld.h:770
const std::vector< uint8_t > & tile16_blockset_data() const
Definition overworld.h:825
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:844
absl::StatusOr< OverworldSpriteSavePlan > PrepareSpriteSave() const
auto mutable_overworld_map(int i)
Definition overworld.h:776
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:287
absl::Status SaveOverworldMaps()
Save compressed map tile data to ROM.
auto mutable_sprites(int state)
Definition overworld.h:792
void set_game_state(int state)
Definition overworld.cc:154
const std::array< uint16_t, 256 > & lyra_a0_tiles() const
Definition overworld.h:460
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:803
absl::Status SaveMap16Expanded()
Save expanded tile16 definitions (v1+ ROMs)
absl::StatusOr< std::array< uint8_t, kNumTileTypes > > GetTile8CollisionTypes(int map_id) const
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:754
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
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
EmptyStateOptions EmptyLoading(const char *what, bool compact)
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
EmptyStateOptions EmptyNoRom(bool compact)
constexpr uint32_t kLyraA0LowStreamPc
constexpr uint32_t kLyraA0HighStreamPc
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:246
constexpr int kNumTile16Individual
Definition overworld.h:244
constexpr int kNumOverworldMaps
Definition common.h:85
absl::Status ApplyOverworldSpriteSave(Rom &rom, const OverworldSpriteSavePlan &plan)
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:90
OverworldRomProfile DetectOverworldRomProfile(const Rom &rom)
Definition overworld.h:325
absl::StatusOr< OverworldEntranceTileTypes > LoadEntranceTileTypes(Rom *rom)
int WorldForOverworldMap(int map_index)
Definition overworld.h:369
constexpr int kExpandedMapCount
Definition common.h:86
ResolvedGraphicsContext DescribeOverworldGraphicsContext(const OverworldMap &map, const GraphicsContextIdentity &identity)
std::vector< OverworldSpriteWrite > OverworldSpriteSavePlan
#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
static constexpr const char * kDebugWindow
static constexpr const char * kUsageStats
std::vector< ScratchPage > pages
std::vector< int > overworld_tile16_ids
Definition editor.h:120
std::vector< int > tile_ids
Definition tile_brush.h:15
Callbacks for undo integration and map refresh.
std::function< absl::Status(const TileBrush &, int x, int y, int width, int height, bool finalize) paint_project_pattern)
std::function< void(int tile_id)> request_tile16_selection
When set, eyedropper routes here (guarded RequestTileSwitch path).
std::function< absl::StatusOr< uint16_t >(int world, int x, int y)> read_project_tile
std::function< void(int map_index)> refresh_overworld_map_on_demand
std::function< void()> scroll_blockset_to_current_tile
std::function< int(int world)> allocated_rows
std::function< void()> finalize_paint_operation
std::function< void(int map_id)> select_map_for_editing
std::function< bool(int world, int x, int y)> is_project_tile
std::function< const gfx::Bitmap *(int map_id)> project_tile_atlas
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:229
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
core::HackManifest hack_manifest
Definition project.h:376