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"
17#include "app/gfx/core/bitmap.h"
23#include "app/gui/core/color.h"
24#include "app/gui/core/icons.h"
25#include "app/gui/core/input.h"
27#include "imgui/imgui.h"
28#include "util/file_util.h"
29#include "util/hex.h"
30#include "util/macro.h"
31
32namespace yaze {
33namespace editor {
34
37 return;
38 auto* window_manager = dependencies_.window_manager;
39
40 window_manager->RegisterPanel(
41 {.card_id = "screen.dungeon_maps",
42 .display_name = "Dungeon Maps",
43 .window_title = " Dungeon Map Editor",
44 .icon = ICON_MD_MAP,
45 .category = "Screen",
46 .shortcut_hint = "Alt+1",
47 .priority = 10,
48 .enabled_condition = [this]() { return rom()->is_loaded(); },
49 .disabled_tooltip = "Load a ROM first"});
50 window_manager->RegisterPanel(
51 {.card_id = "screen.inventory_menu",
52 .display_name = "Inventory Menu",
53 .window_title = " Inventory Menu",
54 .icon = ICON_MD_INVENTORY,
55 .category = "Screen",
56 .shortcut_hint = "Alt+2",
57 .priority = 20,
58 .enabled_condition = [this]() { return rom()->is_loaded(); },
59 .disabled_tooltip = "Load a ROM first"});
60 window_manager->RegisterPanel(
61 {.card_id = "screen.overworld_map",
62 .display_name = "Overworld Map",
63 .window_title = " Overworld Map",
64 .icon = ICON_MD_PUBLIC,
65 .category = "Screen",
66 .shortcut_hint = "Alt+3",
67 .priority = 30,
68 .enabled_condition = [this]() { return rom()->is_loaded(); },
69 .disabled_tooltip = "Load a ROM first"});
70 window_manager->RegisterPanel(
71 {.card_id = "screen.title_screen",
72 .display_name = "Title Screen",
73 .window_title = " Title Screen",
74 .icon = ICON_MD_TITLE,
75 .category = "Screen",
76 .shortcut_hint = "Alt+4",
77 .priority = 40,
78 .enabled_condition = [this]() { return rom()->is_loaded(); },
79 .disabled_tooltip = "Load a ROM first"});
80 window_manager->RegisterPanel(
81 {.card_id = "screen.naming_screen",
82 .display_name = "Naming Screen",
83 .window_title = " Naming Screen",
84 .icon = ICON_MD_EDIT,
85 .category = "Screen",
86 .shortcut_hint = "Alt+5",
87 .priority = 50,
88 .enabled_condition = [this]() { return rom()->is_loaded(); },
89 .disabled_tooltip = "Load a ROM first"});
90 // "screen.menu_tilemap" is registered below via RegisterWindowContent()
91 // only (no RegisterPanel() literal): as an EmbeddedTool (see
92 // MenuTilemapPanel::GetPresentationPolicy()), its full WindowDescriptor
93 // -- including presentation/workflow/shortcut/size metadata -- is
94 // derived from the panel's own virtual overrides (the post-#262
95 // admission-policy pattern), not hand-duplicated here.
96
97 // Register WindowContent implementations
98 window_manager->RegisterWindowContent(std::make_unique<DungeonMapsPanel>(
99 [this]() { DrawDungeonMapsEditor(); }));
100 window_manager->RegisterWindowContent(std::make_unique<InventoryMenuPanel>(
101 [this]() { DrawInventoryMenuEditor(); }));
102 window_manager->RegisterWindowContent(
103 std::make_unique<OverworldMapScreenPanel>(
104 [this]() { DrawOverworldMapEditor(); }));
105 window_manager->RegisterWindowContent(std::make_unique<TitleScreenPanel>(
106 [this]() { DrawTitleScreenEditor(); }));
107 window_manager->RegisterWindowContent(std::make_unique<NamingScreenPanel>(
108 [this]() { DrawNamingScreenEditor(); }));
109 window_manager->RegisterWindowContent(std::make_unique<MenuTilemapPanel>(
110 [this]() { DrawMenuTilemapEditor(); },
111 [this]() { return rom() && rom()->is_loaded(); }));
112
113 // Show title screen by default
114 window_manager->OpenWindow("screen.title_screen");
115}
116
117absl::Status ScreenEditor::Load() {
118 gfx::ScopedTimer timer("ScreenEditor::Load");
120 if (!rom_ || !rom_->is_loaded() || !game_data()) {
121 return absl::FailedPreconditionError(
122 "Screen Editor requires a loaded ROM and GameData");
123 }
124 if (game_data()->palette_groups.dungeon_main.size() <= 3) {
125 return absl::FailedPreconditionError(
126 "Screen Editor requires dungeon palette 3");
127 }
128
133 game_data()->graphics_buffer, false));
134
135 // Load graphics sheets and apply dungeon palette
136 // Recreate sheet contents in place. Arena commands retain Bitmap*, so the
137 // unique_ptr owners must remain stable across reloads.
138 for (int i = 0; i < 4; i++) {
139 const auto pixels = game_data()->sheet_store.Sheet(212 + i);
140 if (pixels.size() != 128 * 32) {
141 return absl::FailedPreconditionError(absl::StrFormat(
142 "Screen Dungeon Map sheet %d is unavailable in this ROM session",
143 212 + i));
144 }
145 auto* sheet = GetOrCreateSheet(i);
146 sheet->Create(128, 32, 8,
147 std::vector<uint8_t>(pixels.begin(), pixels.end()));
148 sheet->SetPalette(
149 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
152 }
153
154 // Create a single tilemap for tile8 graphics with on-demand texture creation
155 // Combine all 4 sheets (128x32 each) into one bitmap (128x128)
156 // This gives us 16 tiles per row × 16 rows = 256 tiles total
157 const int tile8_width = 128;
158 const int tile8_height = 128; // 4 sheets × 32 pixels each
159 std::vector<uint8_t> tile8_data(tile8_width * tile8_height);
160
161 // Copy data from all 4 sheets into the combined bitmap
162 for (int sheet_idx = 0; sheet_idx < 4; sheet_idx++) {
163 const auto& sheet = *sheets_[sheet_idx];
164 int dest_y_offset = sheet_idx * 32; // Each sheet is 32 pixels tall
165
166 for (int y = 0; y < 32; y++) {
167 for (int x = 0; x < 128; x++) {
168 int src_index = y * 128 + x;
169 int dest_index = (dest_y_offset + y) * 128 + x;
170
171 if (src_index < sheet.size() && dest_index < tile8_data.size()) {
172 tile8_data[dest_index] = sheet.data()[src_index];
173 }
174 }
175 }
176 }
177
178 // Create tilemap with 8x8 tile size
179 tile8_tilemap_.tile_size = {8, 8};
180 tile8_tilemap_.map_size = {256, 256}; // Logical size for tile count
181 tile8_tilemap_.atlas.Create(tile8_width, tile8_height, 8, tile8_data);
184
185 // Queue single texture creation for the atlas (not individual tiles)
193 return absl::OkStatus();
194}
195
197 // Rebuild only the source-dependent bitmaps. Keep floor layouts, Tile16
198 // composition words, undo history, and pending save flags in memory.
200 return;
201 }
202 std::vector<uint8_t> atlas_pixels(128 * 128);
203 for (int slot = 0; slot < 4; ++slot) {
204 const auto pixels = game_data()->sheet_store.Sheet(212 + slot);
205 if (pixels.size() != 128 * 32) {
206 return;
207 }
208 std::copy(pixels.begin(), pixels.end(),
209 atlas_pixels.begin() + slot * 128 * 32);
210 auto* sheet = GetOrCreateSheet(slot);
211 sheet->Create(128, 32, 8,
212 std::vector<uint8_t>(pixels.begin(), pixels.end()));
213 sheet->SetPalette(
214 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
217 }
218 tile8_tilemap_.atlas.Create(128, 128, 8, atlas_pixels);
220 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
224
225 // Recompose from the current in-memory Tile16 words, not the ROM table: an
226 // unsaved Tile16 edit must survive the return from Graphics.
227 for (size_t tile_id = 0; tile_id < tile16_blockset_.tile_info.size();
228 ++tile_id) {
229 const auto info = tile16_blockset_.tile_info[tile_id];
230 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer, info[0],
231 info[1], info[2], info[3], 212,
232 static_cast<int>(tile_id));
233 }
237}
238
240 if (!rom_backed_state_valid_ || !rom_ || !rom_->is_loaded() || !game_data()) {
241 return absl::FailedPreconditionError(
242 "Screen Dungeon Map graphics are not loaded");
243 }
246 if (!context.ok()) {
247 return context.status();
248 }
249 auto* manager = static_cast<EditorManager*>(dependencies_.custom_data);
250 if (manager == nullptr) {
251 return absl::FailedPreconditionError("Editor manager is unavailable");
252 }
253 auto* editor_set = manager->GetCurrentEditorSet();
254 if (editor_set == nullptr || editor_set->GetGraphicsEditor() == nullptr) {
255 return absl::FailedPreconditionError("Graphics editor is unavailable");
256 }
257 context->return_to_source = [this, manager] {
259 manager->SwitchToEditor(EditorType::kScreen, true);
261 dependencies_.window_manager->OpenWindow("screen.dungeon_maps");
262 }
263 };
264 RETURN_IF_ERROR(manager->EnsureEditorAssetsLoaded(EditorType::kGraphics));
266 editor_set->GetGraphicsEditor()->OpenInContext(std::move(*context)));
267 manager->SwitchToEditor(EditorType::kGraphics, true);
268 return absl::OkStatus();
269}
270
271absl::StatusOr<zelda3::DungeonMapWritePlan>
275 return absl::FailedPreconditionError(
276 "Save blocked: Screen Editor edits are pending in unsupported domains. "
277 "Dungeon-map Tile16 artwork, title-screen, and overworld pause-map "
278 "writes remain disabled; undo or discard those edits before saving.");
279 }
281 return absl::FailedPreconditionError(
282 "Screen Editor ROM-backed state is invalid; reload Screen assets "
283 "before saving");
284 }
286 return absl::FailedPreconditionError(
287 "Finish the current Screen Editor edit before saving");
288 }
291 }
292 if (!core::FeatureFlags::get().kSaveDungeonMaps) {
293 return absl::FailedPreconditionError(
294 "Save blocked: Screen Editor edits are pending, but Save Dungeon "
295 "Maps is disabled. Enable it or undo the map edits before saving.");
296 }
297 if (!rom_ || !rom_->is_loaded() || loaded_rom_ != rom_ || !game_data() ||
299 return absl::FailedPreconditionError(
300 "Dungeon maps do not belong to the current ROM session; reload "
301 "Screen assets before saving");
302 }
304}
305
308 return absl::FailedPreconditionError("Screen save is already in progress");
309 }
311 map_save_serialized_ = false;
312 return absl::OkStatus();
313}
314
316 // Save never mutates the model, history, or pending flags. The enclosing ROM
317 // transaction restores bytes; there is no editor state to reconstruct.
319 map_save_serialized_ = false;
320}
321
328
329absl::Status ScreenEditor::Save() {
331 if (plan.writes.empty()) {
332 return absl::OkStatus();
333 }
335 // A direct Save writes only the in-memory ROM. Only the outer file-save
336 // commit may mark the editor clean; Undo/Redo remain available after it.
338 return absl::OkStatus();
339}
340
341absl::Status ScreenEditor::Update() {
342 // Panel drawing is handled centrally by WorkspaceWindowManager::DrawAllVisiblePanels()
343 // via the WindowContent implementations registered in Initialize().
344 // No local drawing needed here - this fixes duplicate panel rendering.
345 return status_;
346}
347
349 // Sidebar is now drawn by EditorManager for card-based editors
350 // This method kept for compatibility but sidebar handles card toggles
351}
352
355 ImGui::TextWrapped(
356 tr("Screen assets are unavailable. Reload the active ROM before "
357 "drawing or editing the inventory screen."));
358 return;
359 }
360 if (!inventory_loaded_ && rom()->is_loaded() && game_data()) {
361 status_ = inventory_->Create(rom(), game_data());
362 if (status_.ok()) {
363 palette_ = inventory_->palette();
364 inventory_loaded_ = true;
365 } else {
366 const auto& theme = AgentUI::GetTheme();
367 ImGui::TextColored(theme.text_error_red,
368 tr("Error loading inventory: %s"),
369 status_.message().data());
370 return;
371 }
372 }
373
375
376 if (ImGui::BeginTable("InventoryScreen", 4, ImGuiTableFlags_Resizable)) {
377 ImGui::TableSetupColumn("Canvas");
378 ImGui::TableSetupColumn("Tilesheet");
379 ImGui::TableSetupColumn("Item Icons");
380 ImGui::TableSetupColumn("Palette");
381 ImGui::TableHeadersRow();
382
383 ImGui::TableNextColumn();
384 {
385 gui::CanvasFrameOptions frame_opts;
386 frame_opts.draw_grid = true;
387 frame_opts.grid_step = 32.0f;
388 frame_opts.render_popups = true;
389 auto runtime = gui::BeginCanvas(screen_canvas_, frame_opts);
390 gui::DrawBitmap(runtime, inventory_->bitmap(), 2,
391 inventory_loaded_ ? 1.0f : 0.0f);
392 gui::EndCanvas(screen_canvas_, runtime, frame_opts);
393 }
394
395 ImGui::TableNextColumn();
396 {
397 gui::CanvasFrameOptions frame_opts;
398 frame_opts.canvas_size = ImVec2(128 * 2 + 2, (192 * 2) + 4);
399 frame_opts.draw_grid = true;
400 frame_opts.grid_step = 16.0f;
401 frame_opts.render_popups = true;
402 auto runtime = gui::BeginCanvas(tilesheet_canvas_, frame_opts);
403 gui::DrawBitmap(runtime, inventory_->tilesheet(), 2,
404 inventory_loaded_ ? 1.0f : 0.0f);
405 gui::EndCanvas(tilesheet_canvas_, runtime, frame_opts);
406 }
407
408 ImGui::TableNextColumn();
410
411 ImGui::TableNextColumn();
413
414 ImGui::EndTable();
415 }
416 ImGui::Separator();
417
418 // TODO(scawful): Future Oracle of Secrets menu editor integration
419 // - Full inventory screen layout editor
420 // - Item slot assignment and positioning
421 // - Heart container and magic meter editor
422 // - Equipment display customization
423 // - A/B button equipment quick-select editor
424}
425
427 if (ImGui::BeginTable("InventoryToolset", 8, ImGuiTableFlags_SizingFixedFit,
428 ImVec2(0, 0))) {
429 ImGui::TableSetupColumn("#drawTool");
430 ImGui::TableSetupColumn("#sep1");
431 ImGui::TableSetupColumn("#zoomOut");
432 ImGui::TableSetupColumn("#zoomIN");
433 ImGui::TableSetupColumn("#sep2");
434 ImGui::TableSetupColumn("#bg2Tool");
435 ImGui::TableSetupColumn("#bg3Tool");
436 ImGui::TableSetupColumn("#itemTool");
437
438 ImGui::TableNextColumn();
439 ImGui::BeginDisabled(!undo_manager_.CanUndo());
441 status_ = Undo();
442 }
443 ImGui::EndDisabled();
444 ImGui::TableNextColumn();
445 ImGui::BeginDisabled(!undo_manager_.CanRedo());
447 status_ = Redo();
448 }
449 ImGui::EndDisabled();
450 ImGui::TableNextColumn();
451 ImGui::Text(ICON_MD_MORE_VERT);
452 ImGui::TableNextColumn();
453 if (gui::ToolbarIconButton(ICON_MD_ZOOM_OUT, "Zoom Out")) {
455 }
456 ImGui::TableNextColumn();
457 if (gui::ToolbarIconButton(ICON_MD_ZOOM_IN, "Zoom In")) {
459 }
460 ImGui::TableNextColumn();
461 ImGui::Text(ICON_MD_MORE_VERT);
462 ImGui::TableNextColumn();
463 if (gui::ToolbarIconButton(ICON_MD_DRAW, "Draw Mode")) {
465 }
466 ImGui::TableNextColumn();
467 if (gui::ToolbarIconButton(ICON_MD_BUILD, "Build Mode")) {
468 // current_mode_ = EditingMode::BUILD;
469 }
470
471 ImGui::EndTable();
472 }
473}
474
476 if (ImGui::BeginChild("##ItemIconsList", ImVec2(0, 0), true,
477 ImGuiWindowFlags_HorizontalScrollbar)) {
478 ImGui::Text(tr("Item Icons (2x2 tiles each)"));
479 ImGui::Separator();
480
481 auto& icons = inventory_->item_icons();
482 if (icons.empty()) {
483 ImGui::TextWrapped(
484 tr("No item icons loaded. Icons will be loaded when the "
485 "inventory is initialized."));
486 ImGui::EndChild();
487 return;
488 }
489
490 // Display icons in a table format
491 if (ImGui::BeginTable("##IconsTable", 2,
492 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
493 ImGui::TableSetupColumn("Icon Name");
494 ImGui::TableSetupColumn("Tile Data");
495 ImGui::TableHeadersRow();
496
497 for (size_t i = 0; i < icons.size(); i++) {
498 const auto& icon = icons[i];
499
500 ImGui::TableNextRow();
501 ImGui::TableNextColumn();
502
503 // Display icon name with selectable row
504 if (ImGui::Selectable(icon.name.c_str(), false,
505 ImGuiSelectableFlags_SpanAllColumns)) {
506 // TODO: Select this icon for editing
507 }
508
509 ImGui::TableNextColumn();
510 // Display tile word data in hex format
511 ImGui::Text(tr("TL:%04X TR:%04X"), icon.tile_tl, icon.tile_tr);
512 ImGui::SameLine();
513 ImGui::Text(tr("BL:%04X BR:%04X"), icon.tile_bl, icon.tile_br);
514 }
515
516 ImGui::EndTable();
517 }
518
519 ImGui::Separator();
520 ImGui::TextWrapped(tr(
521 "NOTE: Individual icon editing will be implemented in the future "
522 "Oracle of Secrets menu editor. Each icon is composed of 4 tile words "
523 "representing a 2x2 arrangement of 8x8 tiles in SNES tile format "
524 "(vhopppcc cccccccc)."));
525 }
526 ImGui::EndChild();
527}
528
530 gfx::ScopedTimer timer("screen_editor_draw_dungeon_map_screen");
531
533 ImGui::TextWrapped(tr("No valid dungeon-map floor is loaded."));
534 return;
535 }
536
537 const auto& theme = AgentUI::GetTheme();
538 auto& current_dungeon = dungeon_maps_[selected_dungeon];
539
540 floor_number = i;
541 // The dungeon-map-screen canvas is a tile picker: read-only display of
542 // dungeon room placements with click-to-select for tile16 assignment.
544 screen_canvas_.DrawBackground(ImVec2(325, 325));
546
547 auto boss_room = current_dungeon.boss_room;
548
549 // Pre-allocate vectors for batch operations
550 std::vector<int> tile_ids_to_render;
551 std::vector<ImVec2> tile_positions;
552 tile_ids_to_render.reserve(zelda3::kNumRooms);
553 tile_positions.reserve(zelda3::kNumRooms);
554
555 for (int j = 0; j < zelda3::kNumRooms; j++) {
556 if (current_dungeon.floor_rooms[floor_number][j] != 0x0F) {
557 int tile16_id = current_dungeon.floor_gfx[floor_number][j];
558 int posX = ((j % 5) * 32);
559 int posY = ((j / 5) * 32);
560
561 // Batch tile rendering
562 tile_ids_to_render.push_back(tile16_id);
563 tile_positions.emplace_back(posX * 2, posY * 2);
564 }
565 }
566
567 // Batch render all tiles
568 for (size_t idx = 0; idx < tile_ids_to_render.size(); ++idx) {
569 int tile16_id = tile_ids_to_render[idx];
570 ImVec2 pos = tile_positions[idx];
571
572 // Extract tile data from the atlas directly
573 const int tiles_per_row = tile16_blockset_.atlas.width() / 16;
574 const int tiles_per_column = tile16_blockset_.atlas.height() / 16;
575 if (tiles_per_row <= 0 || tiles_per_column <= 0 || tile16_id < 0 ||
576 tile16_id >= tiles_per_row * tiles_per_column) {
577 continue;
578 }
579 const int tile_x = (tile16_id % tiles_per_row) * 16;
580 const int tile_y = (tile16_id / tiles_per_row) * 16;
581
582 std::vector<uint8_t> tile_data(16 * 16);
583 int tile_data_offset = 0;
584 tile16_blockset_.atlas.Get16x16Tile(tile_x, tile_y, tile_data,
585 tile_data_offset);
586
587 // Create or update cached tile
588 auto* cached_tile = tile16_blockset_.tile_cache.GetTile(tile16_id);
589 if (!cached_tile) {
590 // Create new cached tile
591 gfx::Bitmap new_tile(16, 16, 8, tile_data);
593 tile16_blockset_.tile_cache.CacheTile(tile16_id, std::move(new_tile));
594 cached_tile = tile16_blockset_.tile_cache.GetTile(tile16_id);
595 } else {
596 // Update existing cached tile data
597 cached_tile->set_data(tile_data);
598 }
599
600 if (cached_tile && cached_tile->is_active()) {
601 // Ensure the cached tile has a valid texture
602 if (!cached_tile->texture()) {
603 // Queue texture creation via Arena's deferred system
606 }
607 screen_canvas_.DrawBitmap(*cached_tile, pos.x, pos.y, 4.0F, 255);
608 }
609 }
610
611 // Draw overlays and labels
612 for (int j = 0; j < zelda3::kNumRooms; j++) {
613 if (current_dungeon.floor_rooms[floor_number][j] != 0x0F) {
614 int posX = ((j % 5) * 32);
615 int posY = ((j / 5) * 32);
616
617 if (current_dungeon.floor_rooms[floor_number][j] == boss_room) {
618 screen_canvas_.DrawOutlineWithColor((posX * 2), (posY * 2), 64, 64,
619 theme.status_error);
620 }
621
622 std::string label =
624 screen_canvas_.DrawText(label, (posX * 2), (posY * 2));
625 std::string gfx_id =
626 util::HexByte(current_dungeon.floor_gfx[floor_number][j]);
627 screen_canvas_.DrawText(gfx_id, (posX * 2), (posY * 2) + 16);
628 }
629 }
630
631 screen_canvas_.DrawGrid(64.f, 5);
633
634 if (!screen_canvas_.points().empty()) {
635 int x = screen_canvas_.points().front().x / 64;
636 int y = screen_canvas_.points().front().y / 64;
637 const int room = x + (y * 5);
638 if (room >= 0 && room < zelda3::kNumRooms) {
639 selected_room = static_cast<uint8_t>(room);
640 }
641 }
642}
643
645 gui::AutoWidgetScope widget_scope("Screen/DungeonMap");
647 ImGui::TextWrapped(tr("No valid dungeon map is loaded."));
648 return;
649 }
650 auto& current_dungeon = dungeon_maps_[selected_dungeon];
651 const int floor_count =
652 current_dungeon.nbr_of_floor + current_dungeon.nbr_of_basement;
653 if (floor_count <= 0 ||
654 floor_count > static_cast<int>(current_dungeon.floor_rooms.size()) ||
655 floor_count > static_cast<int>(current_dungeon.floor_gfx.size()) ||
656 floor_count >
657 static_cast<int>(dungeon_map_labels_[selected_dungeon].size())) {
658 ImGui::TextWrapped(tr("This dungeon has no drawable map floors."));
659 return;
660 }
662 floor_number = 0;
663 }
665 selected_room = 0;
666 }
667 if (gui::BeginThemedTabBar("##DungeonMapTabs")) {
668 const int requested_floor = floor_number;
669 for (int i = 0; i < floor_count; i++) {
670 int basement_num = current_dungeon.nbr_of_basement - i;
671 std::string tab_name = absl::StrFormat("Basement %d", basement_num);
672 if (i >= current_dungeon.nbr_of_basement) {
673 tab_name = absl::StrFormat("Floor %d",
674 i - current_dungeon.nbr_of_basement + 1);
675 }
676 const auto flags = select_floor_tab_ && i == requested_floor
677 ? ImGuiTabItemFlags_SetSelected
678 : ImGuiTabItemFlags_None;
679 if (ImGui::BeginTabItem(tab_name.data(), nullptr, flags)) {
681 ImGui::EndTabItem();
682 }
683 }
685 select_floor_tab_ = false;
686 }
687
688 {
689 auto room_before = CaptureDungeonMapSnapshot();
691 "Selected Room",
692 &current_dungeon.floor_rooms[floor_number].at(selected_room))) {
695 current_dungeon.floor_rooms[floor_number][selected_room]);
696 auto after = CaptureDungeonMapSnapshot();
697 undo_manager_.Push(std::make_unique<ScreenEditAction>(
698 room_before, after,
699 [this](const ScreenSnapshot& s) { RestoreFromSnapshot(s); },
700 "Edit room assignment"));
702 }
703 }
704
705 {
706 auto boss_before = CaptureDungeonMapSnapshot();
707 if (gui::InputHexWord("Boss Room", &current_dungeon.boss_room)) {
708 auto after = CaptureDungeonMapSnapshot();
709 undo_manager_.Push(std::make_unique<ScreenEditAction>(
710 boss_before, after,
711 [this](const ScreenSnapshot& s) { RestoreFromSnapshot(s); },
712 "Edit boss room"));
714 }
715 }
716
717 const auto button_size = ImVec2(130, 0);
718
719 if (gui::AutoButton(tr("Add Floor"), button_size)) {
720 status_ = ChangeDungeonMapFloorCount(/*basement=*/false, /*add=*/true);
721 }
722 ImGui::SameLine();
723 if (gui::AutoButton(tr("Remove Floor"), button_size)) {
724 status_ = ChangeDungeonMapFloorCount(/*basement=*/false, /*add=*/false);
725 }
726 if (gui::AutoButton(tr("Add Basement"), button_size)) {
727 status_ = ChangeDungeonMapFloorCount(/*basement=*/true, /*add=*/true);
728 }
729 ImGui::SameLine();
730 if (gui::AutoButton(tr("Remove Basement"), button_size)) {
731 status_ = ChangeDungeonMapFloorCount(/*basement=*/true, /*add=*/false);
732 }
733
734 if (gui::AutoButton(tr("Copy Floor"), button_size)) {
735 copy_button_pressed = true;
736 }
737 ImGui::SameLine();
738 if (gui::AutoButton(tr("Paste Floor"), button_size)) {
740 }
741}
742
760 gfx::ScopedTimer timer("screen_editor_draw_dungeon_maps_room_gfx");
761
769 ImGui::TextWrapped(tr("Dungeon-map graphics are not ready to draw."));
770 return;
771 }
772
773 if (ImGui::BeginChild("##DungeonMapTiles", ImVec2(0, 0), true)) {
774 // Enhanced tilesheet canvas with BeginCanvas/EndCanvas pattern
775 {
776 gui::CanvasFrameOptions tilesheet_opts;
777 tilesheet_opts.canvas_size = ImVec2((256 * 2) + 2, (192 * 2) + 4);
778 tilesheet_opts.draw_grid = true;
779 tilesheet_opts.grid_step = 32.0f;
780 tilesheet_opts.render_popups = true;
781
783 auto tilesheet_rt = gui::BeginCanvas(tilesheet_canvas_, tilesheet_opts);
784
785 // Interactive tile16 selector with grid snapping
786 ImVec2 selected_pos;
787 if (gui::DrawTileSelector(tilesheet_rt, 32, 0, &selected_pos)) {
788 // Double-click detected - handle tile confirmation if needed
789 }
790
791 // Check for single-click selection (legacy compatibility)
793 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
794 if (!tilesheet_canvas_.points().empty()) {
795 const int selected_tile16 = static_cast<int>(
796 tilesheet_canvas_.points().front().x / 32 +
797 (tilesheet_canvas_.points().front().y / 32) * 16);
798
799 if (selected_tile16 >= 0 &&
800 selected_tile16 <
801 static_cast<int>(tile16_blockset_.tile_info.size())) {
802 selected_tile16_ = selected_tile16;
803 // Render selected tile16 and cache tile metadata
806 current_tile16_info.begin());
807 }
808 }
809 }
810
811 // Use stateless bitmap rendering for tilesheet
812 gui::DrawBitmap(tilesheet_rt, tile16_blockset_.atlas, 1, 1, 2.0F, 255);
813
814 gui::EndCanvas(tilesheet_canvas_, tilesheet_rt, tilesheet_opts);
815 }
816
817 if (!tilesheet_canvas_.points().empty() &&
818 !screen_canvas_.points().empty()) {
819 SaveDungeonMapUndoState("Place tile on dungeon map");
824 }
825
826 ImGui::Separator();
827
828 // Current tile canvas with BeginCanvas/EndCanvas pattern
829 {
830 gui::CanvasFrameOptions current_tile_opts;
831 current_tile_opts.draw_grid = true;
832 current_tile_opts.grid_step = 16.0f;
833 current_tile_opts.render_popups = true;
834
836 auto current_tile_rt =
837 gui::BeginCanvas(current_tile_canvas_, current_tile_opts);
838
839 // Get tile8 from cache on-demand (only create texture when needed)
840 if (selected_tile8_ >= 0 && selected_tile8_ < 256) {
841 auto* cached_tile8 = tile8_tilemap_.tile_cache.GetTile(selected_tile8_);
842
843 if (!cached_tile8) {
844 // Extract tile from atlas and cache it
845 const int tiles_per_row =
846 tile8_tilemap_.atlas.width() / 8; // 128 / 8 = 16
847 const int tile_x = (selected_tile8_ % tiles_per_row) * 8;
848 const int tile_y = (selected_tile8_ / tiles_per_row) * 8;
849
850 // Extract 8x8 tile data from atlas
851 std::vector<uint8_t> tile_data(64);
852 for (int py = 0; py < 8; py++) {
853 for (int px = 0; px < 8; px++) {
854 int src_x = tile_x + px;
855 int src_y = tile_y + py;
856 int src_index = src_y * tile8_tilemap_.atlas.width() + src_x;
857 int dst_index = py * 8 + px;
858
859 if (src_index < tile8_tilemap_.atlas.size() && dst_index < 64) {
860 tile_data[dst_index] = tile8_tilemap_.atlas.data()[src_index];
861 }
862 }
863 }
864
865 gfx::Bitmap new_tile8(8, 8, 8, tile_data);
868 std::move(new_tile8));
870 }
871
872 if (cached_tile8 && cached_tile8->is_active()) {
873 // Create texture on-demand only when needed
874 if (!cached_tile8->texture()) {
877 }
878
879 // DrawTilePainter still uses member function (not yet migrated)
880 if (current_tile_canvas_.DrawTilePainter(*cached_tile8, 16)) {
881 // Modify the tile16 based on the selected tile and
882 // current_tile16_info
884 absl::StrFormat("Paint tile16 #%d", selected_tile16_));
885 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
888 212, selected_tile16_);
891 }
892 }
893 }
894
895 // Get selected tile from cache and draw with stateless helper
896 auto* selected_tile =
898 if (selected_tile && selected_tile->is_active()) {
899 // Ensure the selected tile has a valid texture
900 if (!selected_tile->texture()) {
903 }
904 gui::DrawBitmap(current_tile_rt, *selected_tile, 2, 2, 4.0f, 255);
905 }
906
907 gui::EndCanvas(current_tile_canvas_, current_tile_rt, current_tile_opts);
908 }
909
911 ImGui::SameLine();
914 ImGui::SameLine();
916
917 if (ImGui::Button(tr("Modify Tile16")) && selected_tile16_ >= 0 &&
919 static_cast<int>(tile16_blockset_.tile_info.size())) {
921 absl::StrFormat("Modify tile16 #%d", selected_tile16_));
922 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
928 }
929 }
930 ImGui::EndChild();
931}
932
951 ImGui::TextWrapped(
952 tr("Screen assets are unavailable. Reload the active ROM before "
953 "drawing or editing dungeon maps."));
954 return;
955 }
957 ImGui::TextWrapped(tr("No valid dungeon-map model is loaded."));
958 return;
959 }
960
961 // Enhanced editing mode controls with visual feedback
962 if (gui::ToolbarIconButton(ICON_MD_DRAW, "Draw Mode")) {
964 }
965 ImGui::SameLine();
966 if (gui::ToolbarIconButton(ICON_MD_EDIT, "Edit Mode")) {
968 }
969 ImGui::SameLine();
970 ImGui::BeginDisabled();
971 gui::ToolbarIconButton(ICON_MD_SAVE, "Save dungeon map tiles");
972 ImGui::EndDisabled();
973 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
974 ImGui::SetTooltip(
975 tr("Dungeon-map Tile16 saving is disabled until it can participate in "
976 "the coordinated ROM save transaction."));
977 }
978
979 static std::vector<std::string> dungeon_names = {
980 "Sewers/Sanctuary", "Hyrule Castle", "Eastern Palace",
981 "Desert Palace", "Tower of Hera", "Agahnim's Tower",
982 "Palace of Darkness", "Swamp Palace", "Skull Woods",
983 "Thieves' Town", "Ice Palace", "Misery Mire",
984 "Turtle Rock", "Ganon's Tower"};
985
986 if (ImGui::BeginTable("DungeonMapsTable", 4,
987 ImGuiTableFlags_Resizable |
988 ImGuiTableFlags_Reorderable |
989 ImGuiTableFlags_Hideable)) {
990 ImGui::TableSetupColumn("Dungeon");
991 ImGui::TableSetupColumn("Map");
992 ImGui::TableSetupColumn("Rooms Gfx");
993 ImGui::TableSetupColumn("Tiles Gfx");
994 ImGui::TableHeadersRow();
995
996 ImGui::TableNextColumn();
997 const size_t dungeon_count =
998 std::min(dungeon_names.size(), dungeon_maps_.size());
999 for (size_t i = 0; i < dungeon_count; i++) {
1001 selected_dungeon == static_cast<int>(i), "Dungeon Names",
1002 absl::StrFormat("%d", i), dungeon_names[i]);
1003 if (ImGui::IsItemClicked()) {
1004 selected_dungeon = static_cast<int>(i);
1005 }
1006 }
1007
1008 ImGui::TableNextColumn();
1010
1011 ImGui::TableNextColumn();
1013
1014 ImGui::TableNextColumn();
1020 ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
1021 !tilemap_canvas_.points().empty()) {
1022 // Four 128x32 sheets are displayed at 2x: 16 tiles per row, 16 rows.
1023 // Canvas::GetTileIdFromMousePos uses the unscaled 8px grid here, so it
1024 // would select a different tile than the visible 16px highlight.
1025 const auto point = tilemap_canvas_.points().front();
1026 const int x = static_cast<int>(point.x) / 16;
1027 const int y = static_cast<int>(point.y) / 16;
1028 if (x >= 0 && x < 16 && y >= 0 && y < 16) {
1029 selected_tile8_ = x + y * 16;
1030 }
1031 }
1035
1036 ImGui::Text(tr("Selected tile8: %d"), selected_tile8_);
1037 ImGui::SameLine();
1038 if (ImGui::Button(tr("Edit selected graphics tile"))) {
1040 }
1041 if (!status_.ok()) {
1042 ImGui::TextWrapped("%s", std::string(status_.message()).c_str());
1043 }
1044 ImGui::Separator();
1045 ImGui::Text(tr("For use with custom inserted graphics assembly patches."));
1046 if (ImGui::Button(tr("Load GFX from BIN file")))
1047 LoadBinaryGfx();
1048
1049 ImGui::EndTable();
1050 }
1051}
1052
1054 std::string bin_file = util::FileDialogWrapper::ShowOpenFileDialog();
1055 if (!bin_file.empty()) {
1056 std::ifstream file(bin_file, std::ios::binary);
1057 if (file.is_open()) {
1058 // Read the gfx data into a buffer
1059 std::vector<uint8_t> bin_data((std::istreambuf_iterator<char>(file)),
1060 std::istreambuf_iterator<char>());
1061 if (auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4);
1063 converted_bin, true)
1064 .ok()) {
1065 std::vector<std::vector<uint8_t>> gfx_sheets;
1066 for (int i = 0; i < 4; i++) {
1067 gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000),
1068 converted_bin.begin() + ((i + 1) * 0x1000));
1069 auto* sheet = GetOrCreateSheet(i);
1070 sheet->Create(128, 32, 8, gfx_sheets[i]);
1071 sheet->SetPalette(
1072 *game_data()->palette_groups.dungeon_main.mutable_palette(3));
1073 // Queue texture creation via Arena's deferred system
1076 }
1078 binary_gfx_loaded_ = true;
1080 } else {
1081 status_ = absl::InternalError("Failed to load dungeon map tile16");
1082 }
1083 file.close();
1084 }
1085 }
1086}
1087
1090 ImGui::TextWrapped(
1091 tr("Screen assets are unavailable. Reload the active ROM before "
1092 "drawing or editing the title screen."));
1093 return;
1094 }
1095 // Initialize title screen on first draw
1096 if (!title_screen_loaded_ && rom()->is_loaded() && game_data()) {
1098 if (!status_.ok()) {
1099 const auto& theme = AgentUI::GetTheme();
1100 ImGui::TextColored(theme.text_error_red,
1101 tr("Error loading title screen: %s"),
1102 status_.message().data());
1103 return;
1104 }
1105 title_screen_loaded_ = true;
1106 }
1107
1108 if (!title_screen_loaded_) {
1109 ImGui::Text(tr("Title screen not loaded. Ensure ROM is loaded."));
1110 return;
1111 }
1112
1113 // Toolbar with mode controls
1114 if (ImGui::Button(ICON_MD_DRAW)) {
1116 }
1117 ImGui::SameLine();
1118 ImGui::BeginDisabled();
1119 if (ImGui::Button(ICON_MD_SAVE)) {
1121 }
1122 ImGui::EndDisabled();
1123 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1124 ImGui::SetTooltip(
1125 tr("Title-screen ROM saving is disabled until write/reopen/readback "
1126 "verification exists."));
1127 }
1128 ImGui::SameLine();
1129 ImGui::Text(tr("Selected Tile: %d"), selected_title_tile16_);
1130
1131 // Layer visibility controls
1132 bool prev_bg1 = show_title_bg1_;
1133 bool prev_bg2 = show_title_bg2_;
1134 ImGui::Checkbox(tr("Show BG1"), &show_title_bg1_);
1135 ImGui::SameLine();
1136 ImGui::Checkbox(tr("Show BG2"), &show_title_bg2_);
1137
1138 // Re-render composite if visibility changed
1139 if (prev_bg1 != show_title_bg1_ || prev_bg2 != show_title_bg2_) {
1140 status_ =
1142 if (status_.ok()) {
1146 }
1147 }
1148
1149 // Layout: 2-column table (composite view + tile selector)
1150 if (ImGui::BeginTable("TitleScreenTable", 2,
1151 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
1152 ImGui::TableSetupColumn("Title Screen (Composite)");
1153 ImGui::TableSetupColumn("Tile Selector");
1154 ImGui::TableHeadersRow();
1155
1156 // Column 1: Composite Canvas (BG1+BG2 stacked)
1157 ImGui::TableNextColumn();
1159
1160 // Column 2: Blockset Selector
1161 ImGui::TableNextColumn();
1163
1164 ImGui::EndTable();
1165 }
1166}
1167
1169 // The title-screen view stacks BG1 + BG2 into one composite bitmap; the
1170 // canvas is read-only relative to that output (painting goes to the BG1
1171 // tilemap, which then re-renders the composite). Marking both ends lets
1172 // tools tell composite outputs apart from editable scratchpads without
1173 // consulting caller convention.
1177
1178 // Draw composite tilemap (BG1+BG2 stacked with transparency).
1179 // EnsureCompositeBitmapTextureQueued closes the A4 first-frame race: the
1180 // composite has its CREATE queued during TitleScreen::Create(), but if the
1181 // canvas draw runs before ProcessTextureQueue, texture() is still null and
1182 // canvas_rendering's silent guard would drop the draw. The helper also
1183 // pins metadata().purpose so the diagnostic log can name the bitmap.
1184 auto& composite_bitmap = title_screen_.composite_bitmap();
1186 if (composite_bitmap.is_active()) {
1187 title_bg1_canvas_.DrawBitmap(composite_bitmap, 0, 0, 2.0f, 255);
1188 }
1189
1190 // Handle tile painting - always paint to BG1 layer
1193 if (!title_bg1_canvas_.points().empty()) {
1194 auto click_pos = title_bg1_canvas_.points().front();
1195 int tile_x = static_cast<int>(click_pos.x) / 8;
1196 int tile_y = static_cast<int>(click_pos.y) / 8;
1197
1198 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1199 int tilemap_index = tile_y * 32 + tile_x;
1200
1201 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1202 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1203 tile_word |= (title_palette_ & 0x07) << 10;
1204 if (title_h_flip_)
1205 tile_word |= 0x4000;
1206 if (title_v_flip_)
1207 tile_word |= 0x8000;
1208
1209 // Update BG1 buffer and re-render both layers and composite
1210 title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word;
1213 if (status_.ok()) {
1214 // Update BG1 texture
1218
1219 // Re-render and update composite
1222 if (status_.ok()) {
1224 gfx::Arena::TextureCommandType::UPDATE, &composite_bitmap);
1225 }
1226 }
1227 }
1228 }
1229 }
1230 }
1231
1234}
1235
1239
1240 // Draw BG1 tilemap
1241 auto& bg1_bitmap = title_screen_.bg1_bitmap();
1242 if (bg1_bitmap.is_active()) {
1243 title_bg1_canvas_.DrawBitmap(bg1_bitmap, 0, 0, 2.0f, 255);
1244 }
1245
1246 // Handle tile painting
1249 if (!title_bg1_canvas_.points().empty()) {
1250 auto click_pos = title_bg1_canvas_.points().front();
1251 int tile_x = static_cast<int>(click_pos.x) / 8;
1252 int tile_y = static_cast<int>(click_pos.y) / 8;
1253
1254 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1255 int tilemap_index = tile_y * 32 + tile_x;
1256
1257 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1258 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1259 tile_word |= (title_palette_ & 0x07) << 10;
1260 if (title_h_flip_)
1261 tile_word |= 0x4000;
1262 if (title_v_flip_)
1263 tile_word |= 0x8000;
1264
1265 // Update buffer and re-render
1266 title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word;
1269 if (status_.ok()) {
1272 }
1273 }
1274 }
1275 }
1276 }
1277
1280}
1281
1286
1287 // Draw BG2 tilemap
1288 auto& bg2_bitmap = title_screen_.bg2_bitmap();
1289 if (bg2_bitmap.is_active()) {
1290 title_bg2_canvas_.DrawBitmap(bg2_bitmap, 0, 0, 2.0f, 255);
1291 }
1292
1293 // Handle tile painting
1296 if (!title_bg2_canvas_.points().empty()) {
1297 auto click_pos = title_bg2_canvas_.points().front();
1298 int tile_x = static_cast<int>(click_pos.x) / 8;
1299 int tile_y = static_cast<int>(click_pos.y) / 8;
1300
1301 if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) {
1302 int tilemap_index = tile_y * 32 + tile_x;
1303
1304 // Create tile word: tile_id | (palette << 10) | h_flip | v_flip
1305 uint16_t tile_word = selected_title_tile16_ & 0x3FF;
1306 tile_word |= (title_palette_ & 0x07) << 10;
1307 if (title_h_flip_)
1308 tile_word |= 0x4000;
1309 if (title_v_flip_)
1310 tile_word |= 0x8000;
1311
1312 // Update buffer and re-render
1313 title_screen_.mutable_bg2_buffer()[tilemap_index] = tile_word;
1316 if (status_.ok()) {
1319 }
1320 }
1321 }
1322 }
1323 }
1324
1327}
1328
1333
1334 // Draw tile8 bitmap (8x8 tiles used to compose tile16)
1335 auto& tiles8_bitmap = title_screen_.tiles8_bitmap();
1336 if (tiles8_bitmap.is_active()) {
1337 title_blockset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255);
1338 }
1339
1340 // Handle tile selection (8x8 tiles)
1342 // Calculate selected tile ID from click position
1343 if (!title_blockset_canvas_.points().empty()) {
1344 auto click_pos = title_blockset_canvas_.points().front();
1345 int tile_x = static_cast<int>(click_pos.x) / 8;
1346 int tile_y = static_cast<int>(click_pos.y) / 8;
1347 int tiles_per_row = 128 / 8; // 16 tiles per row for 8x8 tiles
1348 selected_title_tile16_ = tile_x + (tile_y * tiles_per_row);
1349 }
1350 }
1351
1354
1355 // Show selected tile preview and controls
1356 if (selected_title_tile16_ >= 0) {
1357 ImGui::Text(tr("Selected Tile: %d"), selected_title_tile16_);
1358
1359 // Flip controls
1360 ImGui::Checkbox(tr("H Flip"), &title_h_flip_);
1361 ImGui::SameLine();
1362 ImGui::Checkbox(tr("V Flip"), &title_v_flip_);
1363
1364 // Palette selector (0-7 for 3BPP graphics)
1365 ImGui::SetNextItemWidth(100);
1366 ImGui::SliderInt(tr("Palette"), &title_palette_, 0, 7);
1367 }
1368}
1369
1371
1375
1378 if (menu_tilemap_ui_.loaded()) {
1379 snapshot.metadata.push_back(
1380 {.id = "menu_tilemap_file",
1381 .label = "Menu tilemap",
1383 if (menu_tilemap_ui_.dirty()) {
1384 snapshot.has_pending_changes = true;
1385 snapshot.pending_label = "Menu tilemap has unsaved edits";
1386 }
1387 }
1388 return snapshot;
1389}
1390
1393 ImGui::TextWrapped(
1394 tr("Screen assets are unavailable. Reload the active ROM before "
1395 "drawing or editing the pause-map screen."));
1396 return;
1397 }
1398 // Initialize overworld map on first draw
1399 if (!ow_map_loaded_ && rom()->is_loaded()) {
1401 if (!status_.ok()) {
1402 const auto& theme = AgentUI::GetTheme();
1403 ImGui::TextColored(theme.text_error_red,
1404 tr("Error loading overworld map: %s"),
1405 status_.message().data());
1406 return;
1407 }
1408 ow_map_loaded_ = true;
1409 }
1410
1411 if (!ow_map_loaded_) {
1412 ImGui::Text(tr("Overworld map not loaded. Ensure ROM is loaded."));
1413 return;
1414 }
1415
1416 // Toolbar with mode controls. Keep this explicit: beta users confused this
1417 // panel with the main Overworld Editor, but it edits the pause-menu world map
1418 // art.
1419 if (ImGui::Button(tr("Paint Mode"))) {
1421 }
1422 if (ImGui::IsItemHovered()) {
1423 ImGui::SetTooltip(tr(
1424 "Paint the pause-menu world map: choose an 8x8 tile in Tileset, then "
1425 "click Map Canvas."));
1426 }
1427 ImGui::SameLine();
1428 ImGui::BeginDisabled();
1429 if (ImGui::Button(tr("Save World Map"))) {
1431 }
1432 ImGui::EndDisabled();
1433 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1434 ImGui::SetTooltip(tr(
1435 "Pause-map ROM saving is disabled until Light World, Dark World, and "
1436 "palette writes pass write/reopen/readback verification."));
1437 }
1438 ImGui::SameLine();
1439
1440 // World toggle
1441 if (ImGui::Button(ow_show_dark_world_ ? "Dark World" : "Light World")) {
1443 // Re-render map with new world
1445 if (status_.ok()) {
1448 }
1449 }
1450 ImGui::SameLine();
1451
1452 // Custom map load/save buttons
1453 if (ImGui::Button(tr("Load Custom Map..."))) {
1455 if (!path.empty()) {
1457 if (!status_.ok()) {
1458 ImGui::OpenPopup("CustomMapLoadError");
1459 } else {
1461 }
1462 }
1463 }
1464 ImGui::SameLine();
1465 if (ImGui::Button(tr("Save Custom Map..."))) {
1467 if (!path.empty()) {
1469 if (status_.ok()) {
1470 ImGui::OpenPopup("CustomMapSaveSuccess");
1471 }
1472 }
1473 }
1474
1475 ImGui::SameLine();
1476 ImGui::Text(tr("Selected Tile: %d"), selected_ow_tile_);
1477
1478 ImGui::TextWrapped(tr(
1479 "This edits the pause-menu world map art, not the 160 playable "
1480 "overworld areas. For area-map painting, entrances, exits, items, and "
1481 "sprites, use Overworld Editor. Paint flow here: pick an 8x8 tile from "
1482 "Tileset, then click Map Canvas; use Light/Dark World to choose the "
1483 "target map."));
1484 ImGui::Separator();
1485
1486 // Custom map error/success popups
1487 if (ImGui::BeginPopup("CustomMapLoadError")) {
1488 ImGui::Text(tr("Error loading custom map: %s"), status_.message().data());
1489 ImGui::EndPopup();
1490 }
1491 if (ImGui::BeginPopup("CustomMapSaveSuccess")) {
1492 ImGui::Text(tr("Custom map saved successfully!"));
1493 ImGui::EndPopup();
1494 }
1495
1496 // Layout: 3-column table
1497 if (ImGui::BeginTable("OWMapTable", 3,
1498 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
1499 ImGui::TableSetupColumn("Map Canvas");
1500 ImGui::TableSetupColumn("Tileset");
1501 ImGui::TableSetupColumn("Palette");
1502 ImGui::TableHeadersRow();
1503
1504 // Column 1: Map Canvas
1505 ImGui::TableNextColumn();
1509
1510 auto& map_bitmap = ow_map_screen_.map_bitmap();
1511 if (map_bitmap.is_active()) {
1512 ow_map_canvas_.DrawBitmap(map_bitmap, 0, 0, 1.0f, 255);
1513 }
1514
1515 // Handle tile painting
1517 if (ow_map_canvas_.DrawTileSelector(8.0f)) {
1518 if (!ow_map_canvas_.points().empty()) {
1519 auto click_pos = ow_map_canvas_.points().front();
1520 int tile_x = static_cast<int>(click_pos.x) / 8;
1521 int tile_y = static_cast<int>(click_pos.y) / 8;
1522
1523 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
1524 int tile_index = tile_x + (tile_y * 64);
1525
1526 // Update appropriate world's tile data
1527 if (ow_show_dark_world_) {
1529 } else {
1531 }
1533
1534 // Re-render map
1536 if (status_.ok()) {
1539 }
1540 }
1541 }
1542 }
1543 }
1544
1547
1548 // Column 2: Tileset Selector
1549 ImGui::TableNextColumn();
1553
1554 auto& tiles8_bitmap = ow_map_screen_.tiles8_bitmap();
1555 if (tiles8_bitmap.is_active()) {
1556 ow_tileset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255);
1557 }
1558
1559 // Handle tile selection
1561 if (!ow_tileset_canvas_.points().empty()) {
1562 auto click_pos = ow_tileset_canvas_.points().front();
1563 int tile_x = static_cast<int>(click_pos.x) / 8;
1564 int tile_y = static_cast<int>(click_pos.y) / 8;
1565 selected_ow_tile_ = tile_x + (tile_y * 16); // 16 tiles per row
1566 }
1567 }
1568
1571
1572 // Column 3: Palette Display
1573 ImGui::TableNextColumn();
1576 // Use inline palette editor for full 128-color palette
1577 const auto palette_before = palette;
1578 gui::InlinePaletteEditor(palette, "Overworld Map Palette");
1579 if (palette != palette_before) {
1581 }
1582
1583 ImGui::EndTable();
1584 }
1585}
1586
1588 static bool show_bg1 = true;
1589 static bool show_bg2 = true;
1590 static bool show_bg3 = true;
1591
1592 static bool drawing_bg1 = true;
1593 static bool drawing_bg2 = false;
1594 static bool drawing_bg3 = false;
1595
1596 ImGui::Checkbox(tr("Show BG1"), &show_bg1);
1597 ImGui::SameLine();
1598 ImGui::Checkbox(tr("Show BG2"), &show_bg2);
1599
1600 ImGui::Checkbox(tr("Draw BG1"), &drawing_bg1);
1601 ImGui::SameLine();
1602 ImGui::Checkbox(tr("Draw BG2"), &drawing_bg2);
1603 ImGui::SameLine();
1604 ImGui::Checkbox(tr("Draw BG3"), &drawing_bg3);
1605}
1606
1607// ---------------------------------------------------------------------------
1608// Undo/redo helpers
1609// ---------------------------------------------------------------------------
1610
1611absl::Status ScreenEditor::ChangeDungeonMapFloorCount(bool basement, bool add) {
1614 return absl::FailedPreconditionError("Dungeon map edit is unavailable");
1615 }
1616 auto& map = dungeon_maps_[selected_dungeon];
1617 auto& labels = dungeon_map_labels_[selected_dungeon];
1618 const size_t total = map.nbr_of_basement + map.nbr_of_floor;
1619 const int count = basement ? map.nbr_of_basement : map.nbr_of_floor;
1620 if (map.floor_rooms.size() != total || map.floor_gfx.size() != total ||
1621 labels.size() != total) {
1622 return absl::InvalidArgumentError(
1623 "Dungeon map floor vectors are inconsistent");
1624 }
1625 if ((add && count >= 8) || (!add && (count == 0 || total <= 1))) {
1626 return absl::FailedPreconditionError(
1627 "Keep at least one map floor and at most eight floors of each type");
1628 }
1629 const size_t index = basement ? 0 : (add ? total : total - 1);
1630 if (!add && map.boss_room != 0x000F && map.boss_room != 0xFFFF) {
1631 const auto& rooms = map.floor_rooms[index];
1632 if (std::find(rooms.begin(), rooms.end(), map.boss_room) != rooms.end()) {
1633 return absl::FailedPreconditionError(
1634 "Move or clear the boss before removing its map floor");
1635 }
1636 }
1637 // All refusal paths precede history/model changes. Basement insertion/removal
1638 // acts on the deepest row; upper floors act on the last row.
1640 add ? (basement ? "Add basement" : "Add floor")
1641 : (basement ? "Remove basement" : "Remove floor"));
1642 if (add) {
1643 std::array<uint8_t, zelda3::kNumRooms> rooms, graphics;
1644 std::array<std::string, zelda3::kNumRooms> row_labels;
1645 rooms.fill(0x0F);
1646 graphics.fill(0xFF);
1647 row_labels.fill(util::HexByte(0x0F));
1648 map.floor_rooms.insert(map.floor_rooms.begin() + index, rooms);
1649 map.floor_gfx.insert(map.floor_gfx.begin() + index, graphics);
1650 labels.insert(labels.begin() + index, row_labels);
1651 if (basement && total > 0)
1652 ++floor_number;
1653 } else {
1654 map.floor_rooms.erase(map.floor_rooms.begin() + index);
1655 map.floor_gfx.erase(map.floor_gfx.begin() + index);
1656 labels.erase(labels.begin() + index);
1657 if (basement)
1658 --floor_number;
1659 }
1660 auto& changed_count = basement ? map.nbr_of_basement : map.nbr_of_floor;
1661 changed_count += add ? 1 : -1;
1662 floor_number =
1663 std::clamp(floor_number, 0, static_cast<int>(map.floor_rooms.size()) - 1);
1664 select_floor_tab_ = true;
1666 return absl::OkStatus();
1667}
1668
1681
1689
1690void ScreenEditor::SaveDungeonMapUndoState(const std::string& description) {
1692 pending_dungeon_desc_ = description;
1694}
1695
1696void ScreenEditor::SaveTile16CompUndoState(const std::string& description) {
1698 pending_tile16_desc_ = description;
1700}
1701
1704 return;
1706
1707 auto after = CaptureDungeonMapSnapshot();
1708 undo_manager_.Push(std::make_unique<ScreenEditAction>(
1710 [this](const ScreenSnapshot& snap) { RestoreFromSnapshot(snap); },
1713}
1714
1717 return;
1719
1720 auto after = CaptureTile16CompSnapshot();
1721 undo_manager_.Push(std::make_unique<ScreenEditAction>(
1723 [this](const ScreenSnapshot& snap) { RestoreFromSnapshot(snap); },
1726}
1727
1729 switch (snapshot.edit_type) {
1731 int idx = snapshot.dungeon_map.dungeon_index;
1732 if (idx >= 0 && idx < static_cast<int>(dungeon_maps_.size())) {
1733 dungeon_maps_[idx] = snapshot.dungeon_map.map_data;
1734 dungeon_map_labels_[idx] = snapshot.dungeon_map.labels;
1735 selected_dungeon = idx;
1736 floor_number = std::clamp(
1737 snapshot.dungeon_map.floor_index, 0,
1738 std::max(
1739 0,
1740 static_cast<int>(dungeon_maps_[idx].floor_rooms.size()) - 1));
1741 select_floor_tab_ = true;
1743 }
1744 break;
1745 }
1749 // Re-apply tile16 composition to the blockset
1750 if (game_data()) {
1751 gfx::ModifyTile16(tile16_blockset_, game_data()->graphics_buffer,
1756 }
1758 break;
1759 }
1760 }
1761}
1762
1764 return absl::FailedPreconditionError(
1765 "Title-screen ROM saving is disabled until write/reopen/readback "
1766 "verification exists");
1767}
1768
1770 return absl::FailedPreconditionError(
1771 "Pause-map ROM saving is disabled until Light World, Dark World, and "
1772 "palette writes pass write/reopen/readback verification");
1773}
1774
1778
1780 return Load();
1781}
1782
1784 auto& sheet = sheets_[index];
1785 if (!sheet) {
1786 sheet = std::make_unique<gfx::Bitmap>();
1787 }
1788 return sheet.get();
1789}
1790
1792 if (tilemap.tile_size.x <= 0 || tilemap.tile_size.y <= 0) {
1793 return;
1794 }
1795 for (auto& [tile_id, bitmap] : tilemap.tile_cache.cache_) {
1796 if (!bitmap) {
1797 continue;
1798 }
1799 auto data = gfx::GetTilemapData(tilemap, tile_id);
1800 bitmap->Create(tilemap.tile_size.x, tilemap.tile_size.y, 8, data);
1801 bitmap->SetPalette(tilemap.atlas.palette());
1804 }
1805}
1806
1808 return selected_dungeon >= 0 &&
1809 selected_dungeon < static_cast<int>(dungeon_maps_.size()) &&
1810 selected_dungeon < static_cast<int>(dungeon_map_labels_.size());
1811}
1812
1814 if (!HasValidDungeonSelection() || floor < 0) {
1815 return false;
1816 }
1817 const auto& dungeon = dungeon_maps_[selected_dungeon];
1818 return floor < static_cast<int>(dungeon.floor_rooms.size()) &&
1819 floor < static_cast<int>(dungeon.floor_gfx.size()) &&
1820 floor < static_cast<int>(dungeon_map_labels_[selected_dungeon].size());
1821}
1822
1825 loaded_rom_ = nullptr;
1826 loaded_game_data_ = nullptr;
1828 select_floor_tab_ = true;
1829 dungeon_maps_.clear();
1830 for (auto& labels : dungeon_map_labels_) {
1831 labels.clear();
1832 }
1833
1839 pending_dungeon_desc_.clear();
1840 pending_tile16_desc_.clear();
1846
1847 binary_gfx_loaded_ = false;
1848 inventory_loaded_ = false;
1849 title_screen_loaded_ = false;
1850 ow_map_loaded_ = false;
1851 inventory_->ResetForReload();
1854 palette_.clear();
1856 tile16_blockset_.tile_size = {0, 0};
1857 tile16_blockset_.map_size = {0, 0};
1858 tile8_tilemap_.tile_info.clear();
1859 tile8_tilemap_.tile_size = {0, 0};
1860 tile8_tilemap_.map_size = {0, 0};
1861
1862 selected_room = 0;
1863 selected_tile16_ = 0;
1864 selected_tile8_ = 0;
1865 selected_dungeon = 0;
1866 floor_number = 0;
1867 copy_button_pressed = false;
1868 paste_button_pressed = false;
1871 title_h_flip_ = false;
1872 title_v_flip_ = false;
1873 title_palette_ = 0;
1874 show_title_bg1_ = true;
1875 show_title_bg2_ = true;
1877 ow_show_dark_world_ = false;
1878 for (auto* canvas :
1882 canvas->ClearSelection();
1883 }
1885 status_ = absl::OkStatus();
1886}
1887
1888} // namespace editor
1889} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:225
bool is_loaded() const
Definition rom.h:187
static Flags & get()
Definition features.h:119
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:364
UndoManager undo_manager_
Definition editor.h:371
project::YazeProject * project() const
Definition editor.h:358
zelda3::GameData * game_data() const
Definition editor.h:357
EditorDependencies dependencies_
Definition editor.h:370
virtual EditorContextSnapshot BuildContextSnapshot() const
Definition editor.h:332
void Draw(Rom *rom, zelda3::GameData *game_data, project::YazeProject *project, UndoManager *undo_manager)
void DrawDungeonMapsRoomGfx()
Draw dungeon room graphics editor with enhanced tile16 editing.
absl::Status RefreshRomBackedState()
void RollbackSaveTransaction() override
absl::Status Undo() override
ScreenSnapshot CaptureTile16CompSnapshot() const
absl::Status SaveOverworldMapToRom()
gfx::Bitmap * GetOrCreateSheet(int index)
absl::Status ChangeDungeonMapFloorCount(bool basement, bool add)
ScreenSnapshot pending_dungeon_before_
bool HasPendingOverworldMapChanges() const
absl::Status OpenDungeonMapTileGraphics()
bool HasPendingDungeonMapChanges() const
MenuTilemapEditorUI menu_tilemap_ui_
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::StatusOr< zelda3::DungeonMapWritePlan > PrepareDungeonMapSave() const
zelda3::GameData * loaded_game_data_
absl::Status Redo() override
absl::Status BeginSaveTransaction() override
void SaveTile16CompUndoState(const std::string &description)
bool HasPendingDungeonMapTile16Changes() const
absl::Status SaveTitleScreenToRom()
bool HasPendingTitleScreenChanges() const
zelda3::TitleScreen title_screen_
std::unique_ptr< zelda3::Inventory > inventory_
zelda3::DungeonMapLabels dungeon_map_labels_
EditorContextSnapshot BuildContextSnapshot() const override
void RefreshTileCacheForReload(gfx::Tilemap &tilemap)
void CommitSaveTransaction() override
std::vector< zelda3::DungeonMap > dungeon_maps_
void Push(std::unique_ptr< UndoAction > action)
void RegisterPanel(size_t session_id, const WindowDescriptor &base_info)
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const uint8_t * data() const
Definition bitmap.h:407
const SnesPalette & palette() const
Definition bitmap.h:393
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
auto size() const
Definition bitmap.h:406
int height() const
Definition bitmap.h:399
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:901
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
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:732
RAII timer for automatic timing management.
RAII scope that enables automatic widget registration.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1293
void DrawOutlineWithColor(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1365
ImVector< ImVec2 > * mutable_points()
Definition canvas.h:361
void DrawContextMenu()
Definition canvas.cc:823
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1229
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:1068
CanvasConfig & GetConfig()
Definition canvas.h:243
bool IsMouseHovering() const
Definition canvas.h:354
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:702
void DrawBitmapTable(const BitmapTable &gfx_bin)
Definition canvas.cc:1340
const ImVector< ImVec2 > & points() const
Definition canvas.h:360
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1620
void DrawText(const std::string &text, int x, int y)
Definition canvas.cc:1568
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::Span< const uint8_t > Sheet(uint16_t sheet) const
The sheet's 4096 pixels, or an empty span when it is not held.
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)
absl::StatusOr< GraphicsEditContext > MakeDungeonMapTilesGfxEditContext(const zelda3::GameData *owner, int selected_tile8, bool binary_gfx_loaded)
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:133
void EndCanvas(Canvas &canvas)
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:489
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:161
void EndThemedTabBar()
bool InputTileInfo(const char *label, gfx::TileInfo *tile_info)
Definition input.cc:689
bool AutoButton(const char *label, const ImVec2 &size=ImVec2(0, 0))
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
Definition color.cc:263
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:510
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:33
std::string GetFileName(const std::string &filename)
Gets the filename from a full path.
Definition file_util.cc:19
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.
absl::StatusOr< DungeonMapWritePlan > BuildDungeonMapWritePlan(const std::vector< DungeonMap > &dungeon_maps, size_t rom_size, bool relocated_tile16)
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
std::vector< EditorContextValue > metadata
WorkspaceWindowManager * window_manager
Definition editor.h:182
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:2915
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Automatic widget registration helpers for ImGui Test Engine integration.