yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
screen_editor.cc
Go to the documentation of this file.
1#include "screen_editor.h"
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <fstream>
6#include <iostream>
7#include <memory>
8#include <string>
9
10#include "absl/strings/str_format.h"
14#include "app/gfx/core/bitmap.h"
19#include "app/gui/core/color.h"
20#include "app/gui/core/icons.h"
21#include "app/gui/core/input.h"
23#include "imgui/imgui.h"
24#include "util/file_util.h"
25#include "util/hex.h"
26#include "util/macro.h"
27
28namespace yaze {
29namespace editor {
30
33 return;
34 auto* window_manager = dependencies_.window_manager;
35
36 window_manager->RegisterPanel(
37 {.card_id = "screen.dungeon_maps",
38 .display_name = "Dungeon Maps",
39 .window_title = " Dungeon Map Editor",
40 .icon = ICON_MD_MAP,
41 .category = "Screen",
42 .shortcut_hint = "Alt+1",
43 .priority = 10,
44 .enabled_condition = [this]() { return rom()->is_loaded(); },
45 .disabled_tooltip = "Load a ROM first"});
46 window_manager->RegisterPanel(
47 {.card_id = "screen.inventory_menu",
48 .display_name = "Inventory Menu",
49 .window_title = " Inventory Menu",
50 .icon = ICON_MD_INVENTORY,
51 .category = "Screen",
52 .shortcut_hint = "Alt+2",
53 .priority = 20,
54 .enabled_condition = [this]() { return rom()->is_loaded(); },
55 .disabled_tooltip = "Load a ROM first"});
56 window_manager->RegisterPanel(
57 {.card_id = "screen.overworld_map",
58 .display_name = "Overworld Map",
59 .window_title = " Overworld Map",
60 .icon = ICON_MD_PUBLIC,
61 .category = "Screen",
62 .shortcut_hint = "Alt+3",
63 .priority = 30,
64 .enabled_condition = [this]() { return rom()->is_loaded(); },
65 .disabled_tooltip = "Load a ROM first"});
66 window_manager->RegisterPanel(
67 {.card_id = "screen.title_screen",
68 .display_name = "Title Screen",
69 .window_title = " Title Screen",
70 .icon = ICON_MD_TITLE,
71 .category = "Screen",
72 .shortcut_hint = "Alt+4",
73 .priority = 40,
74 .enabled_condition = [this]() { return rom()->is_loaded(); },
75 .disabled_tooltip = "Load a ROM first"});
76 window_manager->RegisterPanel(
77 {.card_id = "screen.naming_screen",
78 .display_name = "Naming Screen",
79 .window_title = " Naming Screen",
80 .icon = ICON_MD_EDIT,
81 .category = "Screen",
82 .shortcut_hint = "Alt+5",
83 .priority = 50,
84 .enabled_condition = [this]() { return rom()->is_loaded(); },
85 .disabled_tooltip = "Load a ROM first"});
86
87 // Register WindowContent implementations
88 window_manager->RegisterWindowContent(std::make_unique<DungeonMapsPanel>(
89 [this]() { DrawDungeonMapsEditor(); }));
90 window_manager->RegisterWindowContent(std::make_unique<InventoryMenuPanel>(
91 [this]() { DrawInventoryMenuEditor(); }));
92 window_manager->RegisterWindowContent(
93 std::make_unique<OverworldMapScreenPanel>(
94 [this]() { DrawOverworldMapEditor(); }));
95 window_manager->RegisterWindowContent(std::make_unique<TitleScreenPanel>(
96 [this]() { DrawTitleScreenEditor(); }));
97 window_manager->RegisterWindowContent(std::make_unique<NamingScreenPanel>(
98 [this]() { DrawNamingScreenEditor(); }));
99
100 // Show title screen by default
101 window_manager->OpenWindow("screen.title_screen");
102}
103
104absl::Status ScreenEditor::Load() {
105 gfx::ScopedTimer timer("ScreenEditor::Load");
107 if (!rom_ || !rom_->is_loaded() || !game_data()) {
108 return absl::FailedPreconditionError(
109 "Screen Editor requires a loaded ROM and GameData");
110 }
111 if (game_data()->palette_groups.dungeon_main.size() <= 3) {
112 return absl::FailedPreconditionError(
113 "Screen Editor requires dungeon palette 3");
114 }
115
120 game_data()->graphics_buffer, false));
121
122 // Load graphics sheets and apply dungeon palette
123 // Recreate sheet contents in place. Arena commands retain Bitmap*, so the
124 // unique_ptr owners must remain stable across reloads.
125 for (int i = 0; i < 4; i++) {
126 const auto& source = gfx::Arena::Get().gfx_sheets()[212 + i];
127 auto* sheet = GetOrCreateSheet(i);
128 sheet->Create(source.width(), source.height(), source.depth(),
129 source.vector());
130 sheet->metadata() = source.metadata();
131 sheet->SetPalette(
132 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
135 }
136
137 // Create a single tilemap for tile8 graphics with on-demand texture creation
138 // Combine all 4 sheets (128x32 each) into one bitmap (128x128)
139 // This gives us 16 tiles per row × 16 rows = 256 tiles total
140 const int tile8_width = 128;
141 const int tile8_height = 128; // 4 sheets × 32 pixels each
142 std::vector<uint8_t> tile8_data(tile8_width * tile8_height);
143
144 // Copy data from all 4 sheets into the combined bitmap
145 for (int sheet_idx = 0; sheet_idx < 4; sheet_idx++) {
146 const auto& sheet = *sheets_[sheet_idx];
147 int dest_y_offset = sheet_idx * 32; // Each sheet is 32 pixels tall
148
149 for (int y = 0; y < 32; y++) {
150 for (int x = 0; x < 128; x++) {
151 int src_index = y * 128 + x;
152 int dest_index = (dest_y_offset + y) * 128 + x;
153
154 if (src_index < sheet.size() && dest_index < tile8_data.size()) {
155 tile8_data[dest_index] = sheet.data()[src_index];
156 }
157 }
158 }
159 }
160
161 // Create tilemap with 8x8 tile size
162 tile8_tilemap_.tile_size = {8, 8};
163 tile8_tilemap_.map_size = {256, 256}; // Logical size for tile count
164 tile8_tilemap_.atlas.Create(tile8_width, tile8_height, 8, tile8_data);
167
168 // Queue single texture creation for the atlas (not individual tiles)
174 return absl::OkStatus();
175}
176
177absl::Status ScreenEditor::Save() {
179 return absl::FailedPreconditionError(
180 "Screen Editor ROM-backed state is invalid; reload Screen assets "
181 "before saving");
182 }
184 return absl::FailedPreconditionError(
185 "Screen Editor edits cannot all participate safely in the coordinated "
186 "ROM save; title-screen and pause-map ROM writes remain disabled until "
187 "write/reopen/readback verification exists, so discard the pending "
188 "edits before saving the ROM");
189 }
190 if (core::FeatureFlags::get().kSaveDungeonMaps) {
192 }
193 // Title-screen and pause-map controls remain fail-closed until their writers
194 // have write/reopen/readback coverage.
195 return absl::OkStatus();
196}
197
198absl::Status ScreenEditor::Update() {
199 // Panel drawing is handled centrally by WorkspaceWindowManager::DrawAllVisiblePanels()
200 // via the WindowContent implementations registered in Initialize().
201 // No local drawing needed here - this fixes duplicate panel rendering.
202 return status_;
203}
204
206 // Sidebar is now drawn by EditorManager for card-based editors
207 // This method kept for compatibility but sidebar handles card toggles
208}
209
212 ImGui::TextWrapped(
213 tr("Screen assets are unavailable. Reload the active ROM before "
214 "drawing or editing the inventory screen."));
215 return;
216 }
217 if (!inventory_loaded_ && rom()->is_loaded() && game_data()) {
218 status_ = inventory_->Create(rom(), game_data());
219 if (status_.ok()) {
220 palette_ = inventory_->palette();
221 inventory_loaded_ = true;
222 } else {
223 const auto& theme = AgentUI::GetTheme();
224 ImGui::TextColored(theme.text_error_red,
225 tr("Error loading inventory: %s"),
226 status_.message().data());
227 return;
228 }
229 }
230
232
233 if (ImGui::BeginTable("InventoryScreen", 4, ImGuiTableFlags_Resizable)) {
234 ImGui::TableSetupColumn("Canvas");
235 ImGui::TableSetupColumn("Tilesheet");
236 ImGui::TableSetupColumn("Item Icons");
237 ImGui::TableSetupColumn("Palette");
238 ImGui::TableHeadersRow();
239
240 ImGui::TableNextColumn();
241 {
242 gui::CanvasFrameOptions frame_opts;
243 frame_opts.draw_grid = true;
244 frame_opts.grid_step = 32.0f;
245 frame_opts.render_popups = true;
246 auto runtime = gui::BeginCanvas(screen_canvas_, frame_opts);
247 gui::DrawBitmap(runtime, inventory_->bitmap(), 2,
248 inventory_loaded_ ? 1.0f : 0.0f);
249 gui::EndCanvas(screen_canvas_, runtime, frame_opts);
250 }
251
252 ImGui::TableNextColumn();
253 {
254 gui::CanvasFrameOptions frame_opts;
255 frame_opts.canvas_size = ImVec2(128 * 2 + 2, (192 * 2) + 4);
256 frame_opts.draw_grid = true;
257 frame_opts.grid_step = 16.0f;
258 frame_opts.render_popups = true;
259 auto runtime = gui::BeginCanvas(tilesheet_canvas_, frame_opts);
260 gui::DrawBitmap(runtime, inventory_->tilesheet(), 2,
261 inventory_loaded_ ? 1.0f : 0.0f);
262 gui::EndCanvas(tilesheet_canvas_, runtime, frame_opts);
263 }
264
265 ImGui::TableNextColumn();
267
268 ImGui::TableNextColumn();
270
271 ImGui::EndTable();
272 }
273 ImGui::Separator();
274
275 // TODO(scawful): Future Oracle of Secrets menu editor integration
276 // - Full inventory screen layout editor
277 // - Item slot assignment and positioning
278 // - Heart container and magic meter editor
279 // - Equipment display customization
280 // - A/B button equipment quick-select editor
281}
282
284 if (ImGui::BeginTable("InventoryToolset", 8, ImGuiTableFlags_SizingFixedFit,
285 ImVec2(0, 0))) {
286 ImGui::TableSetupColumn("#drawTool");
287 ImGui::TableSetupColumn("#sep1");
288 ImGui::TableSetupColumn("#zoomOut");
289 ImGui::TableSetupColumn("#zoomIN");
290 ImGui::TableSetupColumn("#sep2");
291 ImGui::TableSetupColumn("#bg2Tool");
292 ImGui::TableSetupColumn("#bg3Tool");
293 ImGui::TableSetupColumn("#itemTool");
294
295 ImGui::TableNextColumn();
296 ImGui::BeginDisabled(!undo_manager_.CanUndo());
298 status_ = Undo();
299 }
300 ImGui::EndDisabled();
301 ImGui::TableNextColumn();
302 ImGui::BeginDisabled(!undo_manager_.CanRedo());
304 status_ = Redo();
305 }
306 ImGui::EndDisabled();
307 ImGui::TableNextColumn();
308 ImGui::Text(ICON_MD_MORE_VERT);
309 ImGui::TableNextColumn();
310 if (gui::ToolbarIconButton(ICON_MD_ZOOM_OUT, "Zoom Out")) {
312 }
313 ImGui::TableNextColumn();
314 if (gui::ToolbarIconButton(ICON_MD_ZOOM_IN, "Zoom In")) {
316 }
317 ImGui::TableNextColumn();
318 ImGui::Text(ICON_MD_MORE_VERT);
319 ImGui::TableNextColumn();
320 if (gui::ToolbarIconButton(ICON_MD_DRAW, "Draw Mode")) {
322 }
323 ImGui::TableNextColumn();
324 if (gui::ToolbarIconButton(ICON_MD_BUILD, "Build Mode")) {
325 // current_mode_ = EditingMode::BUILD;
326 }
327
328 ImGui::EndTable();
329 }
330}
331
333 if (ImGui::BeginChild("##ItemIconsList", ImVec2(0, 0), true,
334 ImGuiWindowFlags_HorizontalScrollbar)) {
335 ImGui::Text(tr("Item Icons (2x2 tiles each)"));
336 ImGui::Separator();
337
338 auto& icons = inventory_->item_icons();
339 if (icons.empty()) {
340 ImGui::TextWrapped(
341 tr("No item icons loaded. Icons will be loaded when the "
342 "inventory is initialized."));
343 ImGui::EndChild();
344 return;
345 }
346
347 // Display icons in a table format
348 if (ImGui::BeginTable("##IconsTable", 2,
349 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
350 ImGui::TableSetupColumn("Icon Name");
351 ImGui::TableSetupColumn("Tile Data");
352 ImGui::TableHeadersRow();
353
354 for (size_t i = 0; i < icons.size(); i++) {
355 const auto& icon = icons[i];
356
357 ImGui::TableNextRow();
358 ImGui::TableNextColumn();
359
360 // Display icon name with selectable row
361 if (ImGui::Selectable(icon.name.c_str(), false,
362 ImGuiSelectableFlags_SpanAllColumns)) {
363 // TODO: Select this icon for editing
364 }
365
366 ImGui::TableNextColumn();
367 // Display tile word data in hex format
368 ImGui::Text(tr("TL:%04X TR:%04X"), icon.tile_tl, icon.tile_tr);
369 ImGui::SameLine();
370 ImGui::Text(tr("BL:%04X BR:%04X"), icon.tile_bl, icon.tile_br);
371 }
372
373 ImGui::EndTable();
374 }
375
376 ImGui::Separator();
377 ImGui::TextWrapped(tr(
378 "NOTE: Individual icon editing will be implemented in the future "
379 "Oracle of Secrets menu editor. Each icon is composed of 4 tile words "
380 "representing a 2x2 arrangement of 8x8 tiles in SNES tile format "
381 "(vhopppcc cccccccc)."));
382 }
383 ImGui::EndChild();
384}
385
387 gfx::ScopedTimer timer("screen_editor_draw_dungeon_map_screen");
388
390 ImGui::TextWrapped(tr("No valid dungeon-map floor is loaded."));
391 return;
392 }
393
394 const auto& theme = AgentUI::GetTheme();
395 auto& current_dungeon = dungeon_maps_[selected_dungeon];
396
397 floor_number = i;
398 // The dungeon-map-screen canvas is a tile picker: read-only display of
399 // dungeon room placements with click-to-select for tile16 assignment.
401 screen_canvas_.DrawBackground(ImVec2(325, 325));
403
404 auto boss_room = current_dungeon.boss_room;
405
406 // Pre-allocate vectors for batch operations
407 std::vector<int> tile_ids_to_render;
408 std::vector<ImVec2> tile_positions;
409 tile_ids_to_render.reserve(zelda3::kNumRooms);
410 tile_positions.reserve(zelda3::kNumRooms);
411
412 for (int j = 0; j < zelda3::kNumRooms; j++) {
413 if (current_dungeon.floor_rooms[floor_number][j] != 0x0F) {
414 int tile16_id = current_dungeon.floor_gfx[floor_number][j];
415 int posX = ((j % 5) * 32);
416 int posY = ((j / 5) * 32);
417
418 // Batch tile rendering
419 tile_ids_to_render.push_back(tile16_id);
420 tile_positions.emplace_back(posX * 2, posY * 2);
421 }
422 }
423
424 // Batch render all tiles
425 for (size_t idx = 0; idx < tile_ids_to_render.size(); ++idx) {
426 int tile16_id = tile_ids_to_render[idx];
427 ImVec2 pos = tile_positions[idx];
428
429 // Extract tile data from the atlas directly
430 const int tiles_per_row = tile16_blockset_.atlas.width() / 16;
431 const int tiles_per_column = tile16_blockset_.atlas.height() / 16;
432 if (tiles_per_row <= 0 || tiles_per_column <= 0 || tile16_id < 0 ||
433 tile16_id >= tiles_per_row * tiles_per_column) {
434 continue;
435 }
436 const int tile_x = (tile16_id % tiles_per_row) * 16;
437 const int tile_y = (tile16_id / tiles_per_row) * 16;
438
439 std::vector<uint8_t> tile_data(16 * 16);
440 int tile_data_offset = 0;
441 tile16_blockset_.atlas.Get16x16Tile(tile_x, tile_y, tile_data,
442 tile_data_offset);
443
444 // Create or update cached tile
445 auto* cached_tile = tile16_blockset_.tile_cache.GetTile(tile16_id);
446 if (!cached_tile) {
447 // Create new cached tile
448 gfx::Bitmap new_tile(16, 16, 8, tile_data);
450 tile16_blockset_.tile_cache.CacheTile(tile16_id, std::move(new_tile));
451 cached_tile = tile16_blockset_.tile_cache.GetTile(tile16_id);
452 } else {
453 // Update existing cached tile data
454 cached_tile->set_data(tile_data);
455 }
456
457 if (cached_tile && cached_tile->is_active()) {
458 // Ensure the cached tile has a valid texture
459 if (!cached_tile->texture()) {
460 // Queue texture creation via Arena's deferred system
463 }
464 screen_canvas_.DrawBitmap(*cached_tile, pos.x, pos.y, 4.0F, 255);
465 }
466 }
467
468 // Draw overlays and labels
469 for (int j = 0; j < zelda3::kNumRooms; j++) {
470 if (current_dungeon.floor_rooms[floor_number][j] != 0x0F) {
471 int posX = ((j % 5) * 32);
472 int posY = ((j / 5) * 32);
473
474 if (current_dungeon.floor_rooms[floor_number][j] == boss_room) {
475 screen_canvas_.DrawOutlineWithColor((posX * 2), (posY * 2), 64, 64,
476 theme.status_error);
477 }
478
479 std::string label =
481 screen_canvas_.DrawText(label, (posX * 2), (posY * 2));
482 std::string gfx_id =
483 util::HexByte(current_dungeon.floor_gfx[floor_number][j]);
484 screen_canvas_.DrawText(gfx_id, (posX * 2), (posY * 2) + 16);
485 }
486 }
487
488 screen_canvas_.DrawGrid(64.f, 5);
490
491 if (!screen_canvas_.points().empty()) {
492 int x = screen_canvas_.points().front().x / 64;
493 int y = screen_canvas_.points().front().y / 64;
494 const int room = x + (y * 5);
495 if (room >= 0 && room < zelda3::kNumRooms) {
496 selected_room = static_cast<uint8_t>(room);
497 }
498 }
499}
500
503 ImGui::TextWrapped(tr("No valid dungeon map is loaded."));
504 return;
505 }
506 auto& current_dungeon = dungeon_maps_[selected_dungeon];
507 const int floor_count =
508 current_dungeon.nbr_of_floor + current_dungeon.nbr_of_basement;
509 if (floor_count <= 0 ||
510 floor_count > static_cast<int>(current_dungeon.floor_rooms.size()) ||
511 floor_count > static_cast<int>(current_dungeon.floor_gfx.size()) ||
512 floor_count >
513 static_cast<int>(dungeon_map_labels_[selected_dungeon].size())) {
514 ImGui::TextWrapped(tr("This dungeon has no drawable map floors."));
515 return;
516 }
518 floor_number = 0;
519 }
521 selected_room = 0;
522 }
523 if (gui::BeginThemedTabBar("##DungeonMapTabs")) {
524 for (int i = 0; i < floor_count; i++) {
525 int basement_num = current_dungeon.nbr_of_basement - i;
526 std::string tab_name = absl::StrFormat("Basement %d", basement_num);
527 if (i >= current_dungeon.nbr_of_basement) {
528 tab_name = absl::StrFormat("Floor %d",
529 i - current_dungeon.nbr_of_basement + 1);
530 }
531 if (ImGui::BeginTabItem(tab_name.data())) {
533 ImGui::EndTabItem();
534 }
535 }
537 }
538
539 {
540 auto room_before = CaptureDungeonMapSnapshot();
542 "Selected Room",
543 &current_dungeon.floor_rooms[floor_number].at(selected_room))) {
544 auto after = CaptureDungeonMapSnapshot();
545 undo_manager_.Push(std::make_unique<ScreenEditAction>(
546 room_before, after,
547 [this](const ScreenSnapshot& s) { RestoreFromSnapshot(s); },
548 "Edit room assignment"));
550 }
551 }
552
553 {
554 auto boss_before = CaptureDungeonMapSnapshot();
555 if (gui::InputHexWord("Boss Room", &current_dungeon.boss_room)) {
556 auto after = CaptureDungeonMapSnapshot();
557 undo_manager_.Push(std::make_unique<ScreenEditAction>(
558 boss_before, after,
559 [this](const ScreenSnapshot& s) { RestoreFromSnapshot(s); },
560 "Edit boss room"));
562 }
563 }
564
565 const auto button_size = ImVec2(130, 0);
566
567 if (ImGui::Button(tr("Add Floor"), button_size) &&
568 current_dungeon.nbr_of_floor < 8) {
569 SaveDungeonMapUndoState("Add floor");
570 current_dungeon.nbr_of_floor++;
573 }
574 ImGui::SameLine();
575 if (ImGui::Button(tr("Remove Floor"), button_size) &&
576 current_dungeon.nbr_of_floor > 0) {
577 SaveDungeonMapUndoState("Remove floor");
578 current_dungeon.nbr_of_floor--;
581 }
582
583 if (ImGui::Button(tr("Add Basement"), button_size) &&
584 current_dungeon.nbr_of_basement < 8) {
585 SaveDungeonMapUndoState("Add basement");
586 current_dungeon.nbr_of_basement++;
589 }
590 ImGui::SameLine();
591 if (ImGui::Button(tr("Remove Basement"), button_size) &&
592 current_dungeon.nbr_of_basement > 0) {
593 SaveDungeonMapUndoState("Remove basement");
594 current_dungeon.nbr_of_basement--;
597 }
598
599 if (ImGui::Button(tr("Copy Floor"), button_size)) {
600 copy_button_pressed = true;
601 }
602 ImGui::SameLine();
603 if (ImGui::Button(tr("Paste Floor"), button_size)) {
605 }
606}
607
625 gfx::ScopedTimer timer("screen_editor_draw_dungeon_maps_room_gfx");
626
634 ImGui::TextWrapped(tr("Dungeon-map graphics are not ready to draw."));
635 return;
636 }
637
638 if (ImGui::BeginChild("##DungeonMapTiles", ImVec2(0, 0), true)) {
639 // Enhanced tilesheet canvas with BeginCanvas/EndCanvas pattern
640 {
641 gui::CanvasFrameOptions tilesheet_opts;
642 tilesheet_opts.canvas_size = ImVec2((256 * 2) + 2, (192 * 2) + 4);
643 tilesheet_opts.draw_grid = true;
644 tilesheet_opts.grid_step = 32.0f;
645 tilesheet_opts.render_popups = true;
646
648 auto tilesheet_rt = gui::BeginCanvas(tilesheet_canvas_, tilesheet_opts);
649
650 // Interactive tile16 selector with grid snapping
651 ImVec2 selected_pos;
652 if (gui::DrawTileSelector(tilesheet_rt, 32, 0, &selected_pos)) {
653 // Double-click detected - handle tile confirmation if needed
654 }
655
656 // Check for single-click selection (legacy compatibility)
658 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
659 if (!tilesheet_canvas_.points().empty()) {
660 const int selected_tile16 = static_cast<int>(
661 tilesheet_canvas_.points().front().x / 32 +
662 (tilesheet_canvas_.points().front().y / 32) * 16);
663
664 if (selected_tile16 >= 0 &&
665 selected_tile16 <
666 static_cast<int>(tile16_blockset_.tile_info.size())) {
667 selected_tile16_ = selected_tile16;
668 // Render selected tile16 and cache tile metadata
671 current_tile16_info.begin());
672 }
673 }
674 }
675
676 // Use stateless bitmap rendering for tilesheet
677 gui::DrawBitmap(tilesheet_rt, tile16_blockset_.atlas, 1, 1, 2.0F, 255);
678
679 gui::EndCanvas(tilesheet_canvas_, tilesheet_rt, tilesheet_opts);
680 }
681
682 if (!tilesheet_canvas_.points().empty() &&
683 !screen_canvas_.points().empty()) {
684 SaveDungeonMapUndoState("Place tile on dungeon map");
689 }
690
691 ImGui::Separator();
692
693 // Current tile canvas with BeginCanvas/EndCanvas pattern
694 {
695 gui::CanvasFrameOptions current_tile_opts;
696 current_tile_opts.draw_grid = true;
697 current_tile_opts.grid_step = 16.0f;
698 current_tile_opts.render_popups = true;
699
701 auto current_tile_rt =
702 gui::BeginCanvas(current_tile_canvas_, current_tile_opts);
703
704 // Get tile8 from cache on-demand (only create texture when needed)
705 if (selected_tile8_ >= 0 && selected_tile8_ < 256) {
706 auto* cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
707
708 if (!cached_tile8) {
709 // Extract tile from atlas and cache it
710 const int tiles_per_row =
711 tile8_tilemap_.atlas.width() / 8; // 128 / 8 = 16
712 const int tile_x = (selected_tile8_ % tiles_per_row) * 8;
713 const int tile_y = (selected_tile8_ / tiles_per_row) * 8;
714
715 // Extract 8x8 tile data from atlas
716 std::vector<uint8_t> tile_data(64);
717 for (int py = 0; py < 8; py++) {
718 for (int px = 0; px < 8; px++) {
719 int src_x = tile_x + px;
720 int src_y = tile_y + py;
721 int src_index = src_y * tile8_tilemap_.atlas.width() + src_x;
722 int dst_index = py * 8 + px;
723
724 if (src_index < tile8_tilemap_.atlas.size() && dst_index < 64) {
725 tile_data[dst_index] = tile8_tilemap_.atlas.data()[src_index];
726 }
727 }
728 }
729
730 gfx::Bitmap new_tile8(8, 8, 8, tile_data);
733 std::move(new_tile8));
735 }
736
737 if (cached_tile8 && cached_tile8->is_active()) {
738 // Create texture on-demand only when needed
739 if (!cached_tile8->texture()) {
742 }
743
744 // DrawTilePainter still uses member function (not yet migrated)
745 if (current_tile_canvas_.DrawTilePainter(*cached_tile8, 16)) {
746 // Modify the tile16 based on the selected tile and
747 // current_tile16_info
749 absl::StrFormat("Paint tile16 #%d", selected_tile16_));
750 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
753 212, selected_tile16_);
756 }
757 }
758 }
759
760 // Get selected tile from cache and draw with stateless helper
761 auto* selected_tile =
763 if (selected_tile && selected_tile->is_active()) {
764 // Ensure the selected tile has a valid texture
765 if (!selected_tile->texture()) {
768 }
769 gui::DrawBitmap(current_tile_rt, *selected_tile, 2, 2, 4.0f, 255);
770 }
771
772 gui::EndCanvas(current_tile_canvas_, current_tile_rt, current_tile_opts);
773 }
774
776 ImGui::SameLine();
779 ImGui::SameLine();
781
782 if (ImGui::Button(tr("Modify Tile16")) && selected_tile16_ >= 0 &&
784 static_cast<int>(tile16_blockset_.tile_info.size())) {
786 absl::StrFormat("Modify tile16 #%d", selected_tile16_));
787 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
793 }
794 }
795 ImGui::EndChild();
796}
797
816 ImGui::TextWrapped(
817 tr("Screen assets are unavailable. Reload the active ROM before "
818 "drawing or editing dungeon maps."));
819 return;
820 }
822 ImGui::TextWrapped(tr("No valid dungeon-map model is loaded."));
823 return;
824 }
825
826 // Enhanced editing mode controls with visual feedback
827 if (gui::ToolbarIconButton(ICON_MD_DRAW, "Draw Mode")) {
829 }
830 ImGui::SameLine();
831 if (gui::ToolbarIconButton(ICON_MD_EDIT, "Edit Mode")) {
833 }
834 ImGui::SameLine();
835 ImGui::BeginDisabled();
836 gui::ToolbarIconButton(ICON_MD_SAVE, "Save dungeon map tiles");
837 ImGui::EndDisabled();
838 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
839 ImGui::SetTooltip(
840 tr("Dungeon-map Tile16 saving is disabled until it can participate in "
841 "the coordinated ROM save transaction."));
842 }
843
844 static std::vector<std::string> dungeon_names = {
845 "Sewers/Sanctuary", "Hyrule Castle", "Eastern Palace",
846 "Desert Palace", "Tower of Hera", "Agahnim's Tower",
847 "Palace of Darkness", "Swamp Palace", "Skull Woods",
848 "Thieves' Town", "Ice Palace", "Misery Mire",
849 "Turtle Rock", "Ganon's Tower"};
850
851 if (ImGui::BeginTable("DungeonMapsTable", 4,
852 ImGuiTableFlags_Resizable |
853 ImGuiTableFlags_Reorderable |
854 ImGuiTableFlags_Hideable)) {
855 ImGui::TableSetupColumn("Dungeon");
856 ImGui::TableSetupColumn("Map");
857 ImGui::TableSetupColumn("Rooms Gfx");
858 ImGui::TableSetupColumn("Tiles Gfx");
859 ImGui::TableHeadersRow();
860
861 ImGui::TableNextColumn();
862 const size_t dungeon_count =
863 std::min(dungeon_names.size(), dungeon_maps_.size());
864 for (size_t i = 0; i < dungeon_count; i++) {
866 selected_dungeon == static_cast<int>(i), "Dungeon Names",
867 absl::StrFormat("%d", i), dungeon_names[i]);
868 if (ImGui::IsItemClicked()) {
869 selected_dungeon = static_cast<int>(i);
870 }
871 }
872
873 ImGui::TableNextColumn();
875
876 ImGui::TableNextColumn();
878
879 ImGui::TableNextColumn();
884 // Get the tile8 ID to use for the tile16 drawing above
886 }
890
891 ImGui::Text(tr("Selected tile8: %d"), selected_tile8_);
892 ImGui::Separator();
893 ImGui::Text(tr("For use with custom inserted graphics assembly patches."));
894 if (ImGui::Button(tr("Load GFX from BIN file")))
896
897 ImGui::EndTable();
898 }
899}
900
902 std::string bin_file = util::FileDialogWrapper::ShowOpenFileDialog();
903 if (!bin_file.empty()) {
904 std::ifstream file(bin_file, std::ios::binary);
905 if (file.is_open()) {
906 // Read the gfx data into a buffer
907 std::vector<uint8_t> bin_data((std::istreambuf_iterator<char>(file)),
908 std::istreambuf_iterator<char>());
909 if (auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4);
911 converted_bin, true)
912 .ok()) {
913 std::vector<std::vector<uint8_t>> gfx_sheets;
914 for (int i = 0; i < 4; i++) {
915 gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000),
916 converted_bin.begin() + ((i + 1) * 0x1000));
917 auto* sheet = GetOrCreateSheet(i);
918 sheet->Create(128, 32, 8, gfx_sheets[i]);
919 sheet->SetPalette(
920 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
921 // Queue texture creation via Arena's deferred system
924 }
926 binary_gfx_loaded_ = true;
928 } else {
929 status_ = absl::InternalError("Failed to load dungeon map tile16");
930 }
931 file.close();
932 }
933 }
934}
935
938 ImGui::TextWrapped(
939 tr("Screen assets are unavailable. Reload the active ROM before "
940 "drawing or editing the title screen."));
941 return;
942 }
943 // Initialize title screen on first draw
944 if (!title_screen_loaded_ && rom()->is_loaded() && game_data()) {
946 if (!status_.ok()) {
947 const auto& theme = AgentUI::GetTheme();
948 ImGui::TextColored(theme.text_error_red,
949 tr("Error loading title screen: %s"),
950 status_.message().data());
951 return;
952 }
954 }
955
957 ImGui::Text(tr("Title screen not loaded. Ensure ROM is loaded."));
958 return;
959 }
960
961 // Toolbar with mode controls
962 if (ImGui::Button(ICON_MD_DRAW)) {
964 }
965 ImGui::SameLine();
966 ImGui::BeginDisabled();
967 if (ImGui::Button(ICON_MD_SAVE)) {
969 }
970 ImGui::EndDisabled();
971 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
972 ImGui::SetTooltip(
973 tr("Title-screen ROM saving is disabled until write/reopen/readback "
974 "verification exists."));
975 }
976 ImGui::SameLine();
977 ImGui::Text(tr("Selected Tile: %d"), selected_title_tile16_);
978
979 // Layer visibility controls
980 bool prev_bg1 = show_title_bg1_;
981 bool prev_bg2 = show_title_bg2_;
982 ImGui::Checkbox(tr("Show BG1"), &show_title_bg1_);
983 ImGui::SameLine();
984 ImGui::Checkbox(tr("Show BG2"), &show_title_bg2_);
985
986 // Re-render composite if visibility changed
987 if (prev_bg1 != show_title_bg1_ || prev_bg2 != show_title_bg2_) {
988 status_ =
990 if (status_.ok()) {
994 }
995 }
996
997 // Layout: 2-column table (composite view + tile selector)
998 if (ImGui::BeginTable("TitleScreenTable", 2,
999 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
1000 ImGui::TableSetupColumn("Title Screen (Composite)");
1001 ImGui::TableSetupColumn("Tile Selector");
1002 ImGui::TableHeadersRow();
1003
1004 // Column 1: Composite Canvas (BG1+BG2 stacked)
1005 ImGui::TableNextColumn();
1007
1008 // Column 2: Blockset Selector
1009 ImGui::TableNextColumn();
1011
1012 ImGui::EndTable();
1013 }
1014}
1015
1017 // The title-screen view stacks BG1 + BG2 into one composite bitmap; the
1018 // canvas is read-only relative to that output (painting goes to the BG1
1019 // tilemap, which then re-renders the composite). Marking both ends lets
1020 // tools tell composite outputs apart from editable scratchpads without
1021 // consulting caller convention.
1025
1026 // Draw composite tilemap (BG1+BG2 stacked with transparency).
1027 // EnsureCompositeBitmapTextureQueued closes the A4 first-frame race: the
1028 // composite has its CREATE queued during TitleScreen::Create(), but if the
1029 // canvas draw runs before ProcessTextureQueue, texture() is still null and
1030 // canvas_rendering's silent guard would drop the draw. The helper also
1031 // pins metadata().purpose so the diagnostic log can name the bitmap.
1032 auto& composite_bitmap = title_screen_.composite_bitmap();
1034 if (composite_bitmap.is_active()) {
1035 title_bg1_canvas_.DrawBitmap(composite_bitmap, 0, 0, 2.0f, 255);
1036 }
1037
1038 // Handle tile painting - always paint to BG1 layer
1041 if (!title_bg1_canvas_.points().empty()) {
1042 auto click_pos = title_bg1_canvas_.points().front();
1043 int tile_x = static_cast<int>(click_pos.x) / 8;
1044 int tile_y = static_cast<int>(click_pos.y) / 8;
1045
1046 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1047 int tilemap_index = tile_y * 32 + tile_x;
1048
1049 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1050 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1051 tile_word |= (title_palette_ & 0x07) << 10;
1052 if (title_h_flip_)
1053 tile_word |= 0x4000;
1054 if (title_v_flip_)
1055 tile_word |= 0x8000;
1056
1057 // Update BG1 buffer and re-render both layers and composite
1058 title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word;
1061 if (status_.ok()) {
1062 // Update BG1 texture
1066
1067 // Re-render and update composite
1070 if (status_.ok()) {
1072 gfx::Arena::TextureCommandType::UPDATE, &composite_bitmap);
1073 }
1074 }
1075 }
1076 }
1077 }
1078 }
1079
1082}
1083
1087
1088 // Draw BG1 tilemap
1089 auto& bg1_bitmap = title_screen_.bg1_bitmap();
1090 if (bg1_bitmap.is_active()) {
1091 title_bg1_canvas_.DrawBitmap(bg1_bitmap, 0, 0, 2.0f, 255);
1092 }
1093
1094 // Handle tile painting
1097 if (!title_bg1_canvas_.points().empty()) {
1098 auto click_pos = title_bg1_canvas_.points().front();
1099 int tile_x = static_cast<int>(click_pos.x) / 8;
1100 int tile_y = static_cast<int>(click_pos.y) / 8;
1101
1102 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1103 int tilemap_index = tile_y * 32 + tile_x;
1104
1105 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1106 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1107 tile_word |= (title_palette_ & 0x07) << 10;
1108 if (title_h_flip_)
1109 tile_word |= 0x4000;
1110 if (title_v_flip_)
1111 tile_word |= 0x8000;
1112
1113 // Update buffer and re-render
1114 title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word;
1117 if (status_.ok()) {
1120 }
1121 }
1122 }
1123 }
1124 }
1125
1128}
1129
1134
1135 // Draw BG2 tilemap
1136 auto& bg2_bitmap = title_screen_.bg2_bitmap();
1137 if (bg2_bitmap.is_active()) {
1138 title_bg2_canvas_.DrawBitmap(bg2_bitmap, 0, 0, 2.0f, 255);
1139 }
1140
1141 // Handle tile painting
1144 if (!title_bg2_canvas_.points().empty()) {
1145 auto click_pos = title_bg2_canvas_.points().front();
1146 int tile_x = static_cast<int>(click_pos.x) / 8;
1147 int tile_y = static_cast<int>(click_pos.y) / 8;
1148
1149 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1150 int tilemap_index = tile_y * 32 + tile_x;
1151
1152 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1153 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1154 tile_word |= (title_palette_ & 0x07) << 10;
1155 if (title_h_flip_)
1156 tile_word |= 0x4000;
1157 if (title_v_flip_)
1158 tile_word |= 0x8000;
1159
1160 // Update buffer and re-render
1161 title_screen_.mutable_bg2_buffer()[tilemap_index] = tile_word;
1164 if (status_.ok()) {
1167 }
1168 }
1169 }
1170 }
1171 }
1172
1175}
1176
1181
1182 // Draw tile8 bitmap (8x8 tiles used to compose tile16)
1183 auto& tiles8_bitmap = title_screen_.tiles8_bitmap();
1184 if (tiles8_bitmap.is_active()) {
1185 title_blockset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255);
1186 }
1187
1188 // Handle tile selection (8x8 tiles)
1190 // Calculate selected tile ID from click position
1191 if (!title_blockset_canvas_.points().empty()) {
1192 auto click_pos = title_blockset_canvas_.points().front();
1193 int tile_x = static_cast<int>(click_pos.x) / 8;
1194 int tile_y = static_cast<int>(click_pos.y) / 8;
1195 int tiles_per_row = 128 / 8; // 16 tiles per row for 8x8 tiles
1196 selected_title_tile16_ = tile_x + (tile_y * tiles_per_row);
1197 }
1198 }
1199
1202
1203 // Show selected tile preview and controls
1204 if (selected_title_tile16_ >= 0) {
1205 ImGui::Text(tr("Selected Tile: %d"), selected_title_tile16_);
1206
1207 // Flip controls
1208 ImGui::Checkbox(tr("H Flip"), &title_h_flip_);
1209 ImGui::SameLine();
1210 ImGui::Checkbox(tr("V Flip"), &title_v_flip_);
1211
1212 // Palette selector (0-7 for 3BPP graphics)
1213 ImGui::SetNextItemWidth(100);
1214 ImGui::SliderInt(tr("Palette"), &title_palette_, 0, 7);
1215 }
1216}
1217
1219
1222 ImGui::TextWrapped(
1223 tr("Screen assets are unavailable. Reload the active ROM before "
1224 "drawing or editing the pause-map screen."));
1225 return;
1226 }
1227 // Initialize overworld map on first draw
1228 if (!ow_map_loaded_ && rom()->is_loaded()) {
1230 if (!status_.ok()) {
1231 const auto& theme = AgentUI::GetTheme();
1232 ImGui::TextColored(theme.text_error_red,
1233 tr("Error loading overworld map: %s"),
1234 status_.message().data());
1235 return;
1236 }
1237 ow_map_loaded_ = true;
1238 }
1239
1240 if (!ow_map_loaded_) {
1241 ImGui::Text(tr("Overworld map not loaded. Ensure ROM is loaded."));
1242 return;
1243 }
1244
1245 // Toolbar with mode controls. Keep this explicit: beta users confused this
1246 // panel with the main Overworld Editor, but it edits the pause-menu world map
1247 // art.
1248 if (ImGui::Button(tr("Paint Mode"))) {
1250 }
1251 if (ImGui::IsItemHovered()) {
1252 ImGui::SetTooltip(tr(
1253 "Paint the pause-menu world map: choose an 8x8 tile in Tileset, then "
1254 "click Map Canvas."));
1255 }
1256 ImGui::SameLine();
1257 ImGui::BeginDisabled();
1258 if (ImGui::Button(tr("Save World Map"))) {
1260 }
1261 ImGui::EndDisabled();
1262 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1263 ImGui::SetTooltip(tr(
1264 "Pause-map ROM saving is disabled until Light World, Dark World, and "
1265 "palette writes pass write/reopen/readback verification."));
1266 }
1267 ImGui::SameLine();
1268
1269 // World toggle
1270 if (ImGui::Button(ow_show_dark_world_ ? "Dark World" : "Light World")) {
1272 // Re-render map with new world
1274 if (status_.ok()) {
1277 }
1278 }
1279 ImGui::SameLine();
1280
1281 // Custom map load/save buttons
1282 if (ImGui::Button(tr("Load Custom Map..."))) {
1284 if (!path.empty()) {
1286 if (!status_.ok()) {
1287 ImGui::OpenPopup("CustomMapLoadError");
1288 } else {
1290 }
1291 }
1292 }
1293 ImGui::SameLine();
1294 if (ImGui::Button(tr("Save Custom Map..."))) {
1296 if (!path.empty()) {
1298 if (status_.ok()) {
1299 ImGui::OpenPopup("CustomMapSaveSuccess");
1300 }
1301 }
1302 }
1303
1304 ImGui::SameLine();
1305 ImGui::Text(tr("Selected Tile: %d"), selected_ow_tile_);
1306
1307 ImGui::TextWrapped(tr(
1308 "This edits the pause-menu world map art, not the 160 playable "
1309 "overworld areas. For area-map painting, entrances, exits, items, and "
1310 "sprites, use Overworld Editor. Paint flow here: pick an 8x8 tile from "
1311 "Tileset, then click Map Canvas; use Light/Dark World to choose the "
1312 "target map."));
1313 ImGui::Separator();
1314
1315 // Custom map error/success popups
1316 if (ImGui::BeginPopup("CustomMapLoadError")) {
1317 ImGui::Text(tr("Error loading custom map: %s"), status_.message().data());
1318 ImGui::EndPopup();
1319 }
1320 if (ImGui::BeginPopup("CustomMapSaveSuccess")) {
1321 ImGui::Text(tr("Custom map saved successfully!"));
1322 ImGui::EndPopup();
1323 }
1324
1325 // Layout: 3-column table
1326 if (ImGui::BeginTable("OWMapTable", 3,
1327 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
1328 ImGui::TableSetupColumn("Map Canvas");
1329 ImGui::TableSetupColumn("Tileset");
1330 ImGui::TableSetupColumn("Palette");
1331 ImGui::TableHeadersRow();
1332
1333 // Column 1: Map Canvas
1334 ImGui::TableNextColumn();
1338
1339 auto& map_bitmap = ow_map_screen_.map_bitmap();
1340 if (map_bitmap.is_active()) {
1341 ow_map_canvas_.DrawBitmap(map_bitmap, 0, 0, 1.0f, 255);
1342 }
1343
1344 // Handle tile painting
1346 if (ow_map_canvas_.DrawTileSelector(8.0f)) {
1347 if (!ow_map_canvas_.points().empty()) {
1348 auto click_pos = ow_map_canvas_.points().front();
1349 int tile_x = static_cast<int>(click_pos.x) / 8;
1350 int tile_y = static_cast<int>(click_pos.y) / 8;
1351
1352 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
1353 int tile_index = tile_x + (tile_y * 64);
1354
1355 // Update appropriate world's tile data
1356 if (ow_show_dark_world_) {
1358 } else {
1360 }
1362
1363 // Re-render map
1365 if (status_.ok()) {
1368 }
1369 }
1370 }
1371 }
1372 }
1373
1376
1377 // Column 2: Tileset Selector
1378 ImGui::TableNextColumn();
1382
1383 auto& tiles8_bitmap = ow_map_screen_.tiles8_bitmap();
1384 if (tiles8_bitmap.is_active()) {
1385 ow_tileset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255);
1386 }
1387
1388 // Handle tile selection
1390 if (!ow_tileset_canvas_.points().empty()) {
1391 auto click_pos = ow_tileset_canvas_.points().front();
1392 int tile_x = static_cast<int>(click_pos.x) / 8;
1393 int tile_y = static_cast<int>(click_pos.y) / 8;
1394 selected_ow_tile_ = tile_x + (tile_y * 16); // 16 tiles per row
1395 }
1396 }
1397
1400
1401 // Column 3: Palette Display
1402 ImGui::TableNextColumn();
1405 // Use inline palette editor for full 128-color palette
1406 const auto palette_before = palette;
1407 gui::InlinePaletteEditor(palette, "Overworld Map Palette");
1408 if (palette != palette_before) {
1410 }
1411
1412 ImGui::EndTable();
1413 }
1414}
1415
1417 static bool show_bg1 = true;
1418 static bool show_bg2 = true;
1419 static bool show_bg3 = true;
1420
1421 static bool drawing_bg1 = true;
1422 static bool drawing_bg2 = false;
1423 static bool drawing_bg3 = false;
1424
1425 ImGui::Checkbox(tr("Show BG1"), &show_bg1);
1426 ImGui::SameLine();
1427 ImGui::Checkbox(tr("Show BG2"), &show_bg2);
1428
1429 ImGui::Checkbox(tr("Draw BG1"), &drawing_bg1);
1430 ImGui::SameLine();
1431 ImGui::Checkbox(tr("Draw BG2"), &drawing_bg2);
1432 ImGui::SameLine();
1433 ImGui::Checkbox(tr("Draw BG3"), &drawing_bg3);
1434}
1435
1436// ---------------------------------------------------------------------------
1437// Undo/redo helpers
1438// ---------------------------------------------------------------------------
1439
1451
1459
1460void ScreenEditor::SaveDungeonMapUndoState(const std::string& description) {
1462 pending_dungeon_desc_ = description;
1464}
1465
1466void ScreenEditor::SaveTile16CompUndoState(const std::string& description) {
1468 pending_tile16_desc_ = description;
1470}
1471
1474 return;
1476
1477 auto after = CaptureDungeonMapSnapshot();
1478 undo_manager_.Push(std::make_unique<ScreenEditAction>(
1480 [this](const ScreenSnapshot& snap) { RestoreFromSnapshot(snap); },
1483}
1484
1487 return;
1489
1490 auto after = CaptureTile16CompSnapshot();
1491 undo_manager_.Push(std::make_unique<ScreenEditAction>(
1493 [this](const ScreenSnapshot& snap) { RestoreFromSnapshot(snap); },
1496}
1497
1499 switch (snapshot.edit_type) {
1501 int idx = snapshot.dungeon_map.dungeon_index;
1502 if (idx >= 0 && idx < static_cast<int>(dungeon_maps_.size())) {
1503 dungeon_maps_[idx] = snapshot.dungeon_map.map_data;
1504 dungeon_map_labels_[idx] = snapshot.dungeon_map.labels;
1505 selected_dungeon = idx;
1507 }
1508 break;
1509 }
1513 // Re-apply tile16 composition to the blockset
1514 if (game_data()) {
1515 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
1520 }
1522 break;
1523 }
1524 }
1525}
1526
1528 return absl::FailedPreconditionError(
1529 "Title-screen ROM saving is disabled until write/reopen/readback "
1530 "verification exists");
1531}
1532
1534 return absl::FailedPreconditionError(
1535 "Pause-map ROM saving is disabled until Light World, Dark World, and "
1536 "palette writes pass write/reopen/readback verification");
1537}
1538
1542
1544 return Load();
1545}
1546
1548 auto& sheet = sheets_[index];
1549 if (!sheet) {
1550 sheet = std::make_unique<gfx::Bitmap>();
1551 }
1552 return sheet.get();
1553}
1554
1556 if (tilemap.tile_size.x <= 0 || tilemap.tile_size.y <= 0) {
1557 return;
1558 }
1559 for (auto& [tile_id, bitmap] : tilemap.tile_cache.cache_) {
1560 if (!bitmap) {
1561 continue;
1562 }
1563 auto data = gfx::GetTilemapData(tilemap, tile_id);
1564 bitmap->Create(tilemap.tile_size.x, tilemap.tile_size.y, 8, data);
1565 bitmap->SetPalette(tilemap.atlas.palette());
1568 }
1569}
1570
1572 return selected_dungeon >= 0 &&
1573 selected_dungeon < static_cast<int>(dungeon_maps_.size()) &&
1574 selected_dungeon < static_cast<int>(dungeon_map_labels_.size());
1575}
1576
1578 if (!HasValidDungeonSelection() || floor < 0) {
1579 return false;
1580 }
1581 const auto& dungeon = dungeon_maps_[selected_dungeon];
1582 return floor < static_cast<int>(dungeon.floor_rooms.size()) &&
1583 floor < static_cast<int>(dungeon.floor_gfx.size()) &&
1584 floor < static_cast<int>(dungeon_map_labels_[selected_dungeon].size());
1585}
1586
1589 dungeon_maps_.clear();
1590 for (auto& labels : dungeon_map_labels_) {
1591 labels.clear();
1592 }
1593
1599 pending_dungeon_desc_.clear();
1600 pending_tile16_desc_.clear();
1606
1607 binary_gfx_loaded_ = false;
1608 inventory_loaded_ = false;
1609 title_screen_loaded_ = false;
1610 ow_map_loaded_ = false;
1611 inventory_->ResetForReload();
1614 palette_.clear();
1616 tile16_blockset_.tile_size = {0, 0};
1617 tile16_blockset_.map_size = {0, 0};
1618 tile8_tilemap_.tile_info.clear();
1619 tile8_tilemap_.tile_size = {0, 0};
1620 tile8_tilemap_.map_size = {0, 0};
1621
1622 selected_room = 0;
1623 selected_tile16_ = 0;
1624 selected_tile8_ = 0;
1625 selected_dungeon = 0;
1626 floor_number = 0;
1627 copy_button_pressed = false;
1628 paste_button_pressed = false;
1631 title_h_flip_ = false;
1632 title_v_flip_ = false;
1633 title_palette_ = 0;
1634 show_title_bg1_ = true;
1635 show_title_bg2_ = true;
1637 ow_show_dark_world_ = false;
1638 for (auto* canvas :
1642 canvas->ClearSelection();
1643 }
1645 status_ = absl::OkStatus();
1646}
1647
1648} // namespace editor
1649} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:180
bool is_loaded() const
Definition rom.h:155
static Flags & get()
Definition features.h:119
UndoManager undo_manager_
Definition editor.h:334
zelda3::GameData * game_data() const
Definition editor.h:320
EditorDependencies dependencies_
Definition editor.h:333
void DrawDungeonMapsRoomGfx()
Draw dungeon room graphics editor with enhanced tile16 editing.
absl::Status RefreshRomBackedState()
absl::Status Undo() override
ScreenSnapshot CaptureTile16CompSnapshot() const
absl::Status SaveOverworldMapToRom()
gfx::Bitmap * GetOrCreateSheet(int index)
ScreenSnapshot pending_dungeon_before_
std::array< gfx::TileInfo, 4 > current_tile16_info
absl::Status Save() override
absl::Status Load() override
void SaveDungeonMapUndoState(const std::string &description)
absl::Status Update() override
bool HasValidDungeonFloorSelection(int floor) const
void RestoreFromSnapshot(const ScreenSnapshot &snapshot)
zelda3::OverworldMapScreen ow_map_screen_
ScreenSnapshot pending_tile16_before_
ScreenSnapshot CaptureDungeonMapSnapshot() const
void DrawDungeonMapsEditor()
Draw dungeon maps editor with enhanced ROM hacking features.
absl::Status Redo() override
void SaveTile16CompUndoState(const std::string &description)
absl::Status SaveTitleScreenToRom()
zelda3::TitleScreen title_screen_
std::unique_ptr< zelda3::Inventory > inventory_
zelda3::DungeonMapLabels dungeon_map_labels_
bool HasPendingScreenChanges() const
void RefreshTileCacheForReload(gfx::Tilemap &tilemap)
std::vector< zelda3::DungeonMap > dungeon_maps_
void Push(std::unique_ptr< UndoAction > action)
void RegisterPanel(size_t session_id, const WindowDescriptor &base_info)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const uint8_t * data() const
Definition bitmap.h:398
const SnesPalette & palette() const
Definition bitmap.h:389
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:202
auto size() const
Definition bitmap.h:397
int height() const
Definition bitmap.h:395
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:864
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
int width() const
Definition bitmap.h:394
void Get16x16Tile(int tile_x, int tile_y, std::vector< uint8_t > &tile_data, int &tile_data_offset)
Extract a 16x16 tile from the bitmap (SNES metatile size)
Definition bitmap.cc:703
RAII timer for automatic timing management.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1173
void DrawOutlineWithColor(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1242
ImVector< ImVec2 > * mutable_points()
Definition canvas.h:347
void DrawContextMenu()
Definition canvas.cc:703
int GetTileIdFromMousePos()
Definition canvas.h:319
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1109
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:950
CanvasConfig & GetConfig()
Definition canvas.h:229
bool IsMouseHovering() const
Definition canvas.h:340
void DrawBitmapTable(const BitmapTable &gfx_bin)
Definition canvas.cc:1220
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:613
const ImVector< ImVec2 > & points() const
Definition canvas.h:346
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1495
void DrawText(const std::string &text, int x, int y)
Definition canvas.cc:1443
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
absl::Status SaveCustomMap(const std::string &file_path, bool use_dark_world)
Save map data to external binary file.
absl::Status LoadCustomMap(const std::string &file_path)
Load custom map from external binary file.
absl::Status RenderMapLayer(bool use_dark_world)
Render map tiles into bitmap.
absl::Status Create(Rom *rom)
Initialize and load overworld map data from ROM.
absl::Status Create(Rom *rom, GameData *game_data=nullptr)
Initialize and load title screen data from ROM.
absl::Status RenderCompositeLayer(bool show_bg1, bool show_bg2)
Render composite layer with BG1 on top of BG2 with transparency.
absl::Status RenderBG2Layer()
Render BG2 tilemap into bitmap pixels Converts tile IDs from tiles_bg2_buffer_ into pixel data.
absl::Status RenderBG1Layer()
Render BG1 tilemap into bitmap pixels Converts tile IDs from tiles_bg1_buffer_ into pixel data.
#define ICON_MD_TITLE
Definition icons.h:1990
#define ICON_MD_MORE_VERT
Definition icons.h:1243
#define ICON_MD_DRAW
Definition icons.h:625
#define ICON_MD_ZOOM_OUT
Definition icons.h:2196
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_PUBLIC
Definition icons.h:1524
#define ICON_MD_INVENTORY
Definition icons.h:1011
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_UNDO
Definition icons.h:2039
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
const AgentUITheme & GetTheme()
void EnsureCompositeBitmapTextureQueued(gfx::Bitmap &composite)
void RenderTile16(IRenderer *renderer, Tilemap &tilemap, int tile_id)
Definition tilemap.cc:75
void ModifyTile16(Tilemap &tilemap, const std::vector< uint8_t > &data, const TileInfo &top_left, const TileInfo &top_right, const TileInfo &bottom_left, const TileInfo &bottom_right, int sheet_offset, int tile_id)
Definition tilemap.cc:221
void UpdateTile16(IRenderer *renderer, Tilemap &tilemap, int tile_id)
Definition tilemap.cc:113
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:268
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:132
void EndCanvas(Canvas &canvas)
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:485
bool DrawTileSelector(const CanvasRuntime &rt, int size, int size_y, ImVec2 *out_selected_pos)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void DrawBitmap(const CanvasRuntime &rt, gfx::Bitmap &bitmap, int border_offset=2, float scale=1.0f)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
IMGUI_API absl::Status InlinePaletteEditor(gfx::SnesPalette &palette, const std::string &title, ImGuiColorEditFlags flags)
Full inline palette editor with color picker and copy options.
Definition color.cc:122
void EndThemedTabBar()
bool InputTileInfo(const char *label, gfx::TileInfo *tile_info)
Definition input.cc:685
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
Definition color.cc:239
bool ToolbarIconButton(const char *icon, const char *tooltip, bool is_active)
Convenience wrapper for toolbar-sized icon buttons.
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:506
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom, GameData *game_data, const std::vector< uint8_t > &gfx_data, bool bin_mode)
Load the dungeon map tile16 from the ROM.
constexpr int kNumRooms
Definition dungeon_map.h:48
absl::StatusOr< std::vector< DungeonMap > > LoadDungeonMaps(Rom &rom, DungeonMapLabels &dungeon_map_labels)
Load the dungeon maps from the ROM.
absl::Status SaveDungeonMaps(Rom &rom, std::vector< DungeonMap > &dungeon_maps)
Save the dungeon maps to the ROM.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< std::array< std::string, zelda3::kNumRooms > > labels
WorkspaceWindowManager * window_manager
Definition editor.h:181
Unified screen editor snapshot.
std::array< gfx::TileInfo, 4 > tile_info
int y
Y coordinate or height.
Definition tilemap.h:21
int x
X coordinate or width.
Definition tilemap.h:20
std::unordered_map< int, std::unique_ptr< Bitmap > > cache_
Definition tilemap.h:40
void CacheTile(int tile_id, const Bitmap &bitmap)
Cache a tile bitmap by copying it.
Definition tilemap.h:67
Bitmap * GetTile(int tile_id)
Get a cached tile by ID.
Definition tilemap.h:50
Tilemap structure for SNES tile-based graphics management.
Definition tilemap.h:118
Pair tile_size
Size of individual tiles (8x8 or 16x16)
Definition tilemap.h:123
TileCache tile_cache
Smart tile cache with LRU eviction.
Definition tilemap.h:120
Pair map_size
Size of tilemap in tiles.
Definition tilemap.h:124
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
std::vector< std::array< gfx::TileInfo, 4 > > tile_info
Tile metadata (4 tiles per 16x16)
Definition tilemap.h:122
std::optional< float > grid_step
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2346
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92