yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile16_editor.cc
Go to the documentation of this file.
1#include "tile16_editor.h"
2
3#include <array>
4
5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
13#include "app/gui/core/input.h"
14#include "app/gui/core/style.h"
15#include "imgui/imgui.h"
16#include "rom/rom.h"
17#include "util/hex.h"
18#include "util/log.h"
19#include "util/macro.h"
20#include "zelda3/game_data.h"
22
23namespace yaze {
24namespace editor {
25
26using namespace ImGui;
27
28// Display scales used for the tile8 source/preview rendering.
29constexpr float kTile8DisplayScale = 4.0f;
30
32 const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp,
33 std::array<uint8_t, 0x200>& all_tiles_types) {
34 all_tiles_types_ = all_tiles_types;
35
36 // Copy the graphics bitmap (palette will be set later by overworld editor)
37 current_gfx_bmp_.Create(current_gfx_bmp.width(), current_gfx_bmp.height(),
38 current_gfx_bmp.depth(), current_gfx_bmp.vector());
39 current_gfx_bmp_.SetPalette(current_gfx_bmp.palette()); // Temporary palette
40 // Queue texture for later rendering.
43
44 // Copy the tile16 blockset bitmap
46 tile16_blockset_bmp.width(), tile16_blockset_bmp.height(),
47 tile16_blockset_bmp.depth(), tile16_blockset_bmp.vector());
48 tile16_blockset_bmp_.SetPalette(tile16_blockset_bmp.palette());
49 // Queue texture for later rendering.
52
53 // Note: LoadTile8() will be called after palette is set by overworld editor
54 // This ensures proper palette coordination from the start
55
56 // Initialize current tile16 bitmap - this will be set by SetCurrentTile
58 std::vector<uint8_t>(kTile16PixelCount, 0));
59 current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
60 // Queue texture for later rendering.
63
64 // Initialize enhanced canvas features with proper sizing
67
68 // Attach blockset canvas to the selector widget
71
72 // Configure canvases with proper initialization
75
76 // Initialize enhanced palette editors if ROM is available
77 if (rom_) {
80 }
81
82 // Initialize the current tile16 properly from the blockset
83 if (tile16_blockset_) {
84 RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
85 }
86
88
89 // Setup collision type labels for tile8 canvas
90 ImVector<std::string> tile16_names;
91 for (int i = 0; i < 0x200; ++i) {
92 std::string str = util::HexByte(all_tiles_types_[i]);
93 tile16_names.push_back(str);
94 }
95 *tile8_source_canvas_.mutable_labels(0) = tile16_names;
97
98 // Setup tile info table
100 [&]() { Text("Tile16: %02X", current_tile16_); });
102 [&]() { Text("Tile8: %02X", current_tile8_); });
103 gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
104 Checkbox("X Flip", &x_flip);
105 Checkbox("Y Flip", &y_flip);
106 Checkbox("Priority", &priority_tile);
107 });
108
109 return absl::OkStatus();
110}
111
112absl::Status Tile16Editor::Update() {
114 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
115 }
116
117 if (BeginMenuBar()) {
118 if (BeginMenu("View")) {
119 Checkbox("Show Collision Types",
121 EndMenu();
122 }
123
124 if (BeginMenu("Edit")) {
125 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
127 }
128 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
130 }
131 EndMenu();
132 }
133
134 if (BeginMenu("File")) {
135 if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
137 }
138 if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
140 }
141 Separator();
142 bool live_preview = live_preview_enabled_;
143 if (MenuItem("Live Preview", nullptr, &live_preview)) {
144 EnableLivePreview(live_preview);
145 }
146 EndMenu();
147 }
148
149 if (BeginMenu("Scratch Space")) {
150 for (int i = 0; i < 4; i++) {
151 std::string slot_name = "Slot " + std::to_string(i + 1);
152 if (scratch_space_used_[i]) {
153 if (MenuItem((slot_name + " (Load)").c_str())) {
155 }
156 if (MenuItem((slot_name + " (Save)").c_str())) {
158 }
159 if (MenuItem((slot_name + " (Clear)").c_str())) {
161 }
162 } else {
163 if (MenuItem((slot_name + " (Save)").c_str())) {
165 }
166 }
167 if (i < 3)
168 Separator();
169 }
170 EndMenu();
171 }
172
173 EndMenuBar();
174 }
175
176 // About popup
177 if (BeginPopupModal("About Tile16 Editor", NULL,
178 ImGuiWindowFlags_AlwaysAutoResize)) {
179 Text("Tile16 Editor for Link to the Past");
180 Text("This editor allows you to edit 16x16 tiles used in the game.");
181 Text("Features:");
182 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
183 BulletText("Copy and paste Tile16 graphics");
184 BulletText("Save and load Tile16 graphics to/from scratch space");
185 BulletText("Preview Tile16 graphics at a larger size");
186 Separator();
187 if (Button("Close")) {
188 CloseCurrentPopup();
189 }
190 EndPopup();
191 }
192
193 // Unsaved changes confirmation dialog
195 OpenPopup("Unsaved Changes##Tile16Editor");
196 }
197 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
198 ImGuiWindowFlags_AlwaysAutoResize)) {
199 Text("Tile %d has unsaved changes.", current_tile16_);
200 Text("What would you like to do?");
201 Separator();
202
203 // Save & Continue button (green)
204 PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
205 PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
206 if (Button("Save & Continue", ImVec2(120, 0))) {
207 // Commit just the current tile change
208 if (auto* tile_data = GetCurrentTile16Data()) {
209 auto status =
211 if (status.ok()) {
212 // Remove from pending
215 // Refresh blockset
217 // Now switch to the target tile
220 }
221 }
222 }
225 CloseCurrentPopup();
226 }
227 PopStyleColor(2);
228
229 SameLine();
230
231 // Discard & Continue button (red)
232 PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
233 PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
234 if (Button("Discard & Continue", ImVec2(130, 0))) {
235 // Remove pending changes for current tile
238 // Switch to target tile
241 }
244 CloseCurrentPopup();
245 }
246 PopStyleColor(2);
247
248 SameLine();
249
250 // Cancel button
251 if (Button("Cancel", ImVec2(80, 0))) {
254 CloseCurrentPopup();
255 }
256
257 EndPopup();
258 }
259
260 // Handle keyboard shortcuts
261 if (!ImGui::IsAnyItemActive()) {
262 // Editing shortcuts
263 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
265 }
266 if (ImGui::IsKeyPressed(ImGuiKey_H)) {
268 }
269 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
271 }
272 if (ImGui::IsKeyPressed(ImGuiKey_R)) {
274 }
275 if (ImGui::IsKeyPressed(ImGuiKey_F)) {
276 if (current_tile8_ >= 0 &&
277 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
279 }
280 }
281
282 // Palette shortcuts
283 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
284 status_ = CyclePalette(false);
285 }
286 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
287 status_ = CyclePalette(true);
288 }
289
290 // Palette number shortcuts (1-8)
291 for (int i = 0; i < 8; ++i) {
292 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
294 status_ = CyclePalette(true);
295 status_ = CyclePalette(false);
297 }
298 }
299
300 // Undo/Redo with Ctrl
301 if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
302 ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
303 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
304 status_ = Undo();
305 }
306 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
307 status_ = Redo();
308 }
309 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
311 }
312 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
314 }
315 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
316 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
317 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
319 } else {
321 }
322 }
323 }
324 }
325
327
328 // Draw palette settings popup if enabled
330
331 // Update live preview if dirty
333
334 return absl::OkStatus();
335}
336
338 // REFACTORED: Single unified table layout in UpdateTile16Edit
340}
341
344 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
345 }
346
347 // Menu button for context menu
348 if (Button(ICON_MD_MENU " Menu")) {
349 OpenPopup("##Tile16EditorContextMenu");
350 }
351 SameLine();
352 TextDisabled("Right-click for more options");
353
354 // Context menu
356
357 // About popup
358 if (BeginPopupModal("About Tile16 Editor", NULL,
359 ImGuiWindowFlags_AlwaysAutoResize)) {
360 Text("Tile16 Editor for Link to the Past");
361 Text("This editor allows you to edit 16x16 tiles used in the game.");
362 Text("Features:");
363 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
364 BulletText("Copy and paste Tile16 graphics");
365 BulletText("Save and load Tile16 graphics to/from scratch space");
366 BulletText("Preview Tile16 graphics at a larger size");
367 Separator();
368 if (Button("Close")) {
369 CloseCurrentPopup();
370 }
371 EndPopup();
372 }
373
374 // Unsaved changes confirmation dialog
376 OpenPopup("Unsaved Changes##Tile16Editor");
377 }
378 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
379 ImGuiWindowFlags_AlwaysAutoResize)) {
380 Text("Tile %d has unsaved changes.", current_tile16_);
381 Text("What would you like to do?");
382 Separator();
383
384 PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
385 PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
386 if (Button("Save & Continue", ImVec2(120, 0))) {
387 if (auto* tile_data = GetCurrentTile16Data()) {
388 auto status =
390 if (status.ok()) {
396 }
397 }
398 }
401 CloseCurrentPopup();
402 }
403 PopStyleColor(2);
404
405 SameLine();
406
407 PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
408 PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
409 if (Button("Discard & Continue", ImVec2(130, 0))) {
414 }
417 CloseCurrentPopup();
418 }
419 PopStyleColor(2);
420
421 SameLine();
422
423 if (Button("Cancel", ImVec2(80, 0))) {
426 CloseCurrentPopup();
427 }
428
429 EndPopup();
430 }
431
432 // Handle keyboard shortcuts (same as Update())
433 if (!ImGui::IsAnyItemActive()) {
434 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
436 }
437 if (ImGui::IsKeyPressed(ImGuiKey_H)) {
439 }
440 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
442 }
443 if (ImGui::IsKeyPressed(ImGuiKey_R)) {
445 }
446 if (ImGui::IsKeyPressed(ImGuiKey_F)) {
447 if (current_tile8_ >= 0 &&
448 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
450 }
451 }
452 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
453 status_ = CyclePalette(false);
454 }
455 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
456 status_ = CyclePalette(true);
457 }
458 for (int i = 0; i < 8; ++i) {
459 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
461 status_ = CyclePalette(true);
462 status_ = CyclePalette(false);
464 }
465 }
466 if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
467 ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
468 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
469 status_ = Undo();
470 }
471 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
472 status_ = Redo();
473 }
474 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
476 }
477 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
479 }
480 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
481 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
482 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
484 } else {
486 }
487 }
488 }
489 }
490
494
495 return absl::OkStatus();
496}
497
499 if (BeginPopup("##Tile16EditorContextMenu")) {
500 if (BeginMenu("View")) {
501 Checkbox("Show Collision Types",
503 EndMenu();
504 }
505
506 if (BeginMenu("Edit")) {
507 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
509 }
510 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
512 }
513 Separator();
514 if (MenuItem("Flip Horizontal", "H")) {
516 }
517 if (MenuItem("Flip Vertical", "V")) {
519 }
520 if (MenuItem("Rotate", "R")) {
522 }
523 if (MenuItem("Clear", "Delete")) {
525 }
526 EndMenu();
527 }
528
529 if (BeginMenu("File")) {
530 if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
532 }
533 if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
535 }
536 Separator();
537 bool live_preview = live_preview_enabled_;
538 if (MenuItem("Live Preview", nullptr, &live_preview)) {
539 EnableLivePreview(live_preview);
540 }
541 EndMenu();
542 }
543
544 if (BeginMenu("Scratch Space")) {
545 for (int i = 0; i < 4; i++) {
546 std::string slot_name = "Slot " + std::to_string(i + 1);
547 if (scratch_space_used_[i]) {
548 if (MenuItem((slot_name + " (Load)").c_str())) {
550 }
551 if (MenuItem((slot_name + " (Save)").c_str())) {
553 }
554 if (MenuItem((slot_name + " (Clear)").c_str())) {
556 }
557 } else {
558 if (MenuItem((slot_name + " (Save)").c_str())) {
560 }
561 }
562 if (i < 3)
563 Separator();
564 }
565 EndMenu();
566 }
567
568 EndPopup();
569 }
570}
571
574 gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
575
576 // Configure canvas frame options for blockset view
577 gui::CanvasFrameOptions frame_opts;
578 frame_opts.draw_grid = true;
579 frame_opts.grid_step = 32.0f; // Tile16 grid
580 frame_opts.draw_context_menu = true;
581 frame_opts.draw_overlay = true;
582 frame_opts.render_popups = true;
583 frame_opts.use_child_window = false;
584
585 auto canvas_rt = gui::BeginCanvas(blockset_canvas_, frame_opts);
587
588 // Ensure selector is synced with current selection
591 }
592
593 // Render the selector widget (handles bitmap, grid, highlights, interaction)
595
596 if (result.selection_changed) {
597 // Use RequestTileSwitch to handle pending changes confirmation
598 RequestTileSwitch(result.selected_tile);
599 util::logf("Selected Tile16 from blockset: %d", result.selected_tile);
600 }
601
602 gui::EndCanvas(blockset_canvas_, canvas_rt, frame_opts);
603 EndChild();
604
605 return absl::OkStatus();
606}
607
608// ROM data access methods
610 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 4096) {
611 return nullptr;
612 }
613
614 // Read the current tile16 data from ROM
616 if (!tile_result.ok()) {
617 return nullptr;
618 }
619
620 // Store in instance variable for proper persistence
621 current_tile16_data_ = tile_result.value();
622 return &current_tile16_data_;
623}
624
626 auto* tile_data = GetCurrentTile16Data();
627 if (!tile_data) {
628 return absl::FailedPreconditionError("Cannot access current tile16 data");
629 }
630
631 // Write the modified tile16 data back to ROM
634
635 util::logf("ROM Tile16 data written for tile %d", current_tile16_);
636 return absl::OkStatus();
637}
638
640 if (!tile16_blockset_) {
641 return absl::FailedPreconditionError("Tile16 blockset not available");
642 }
643
644 // CRITICAL FIX: Force regeneration without using problematic tile cache
645 // Directly mark atlas as modified to trigger regeneration from ROM data
646
647 // Mark atlas as modified to trigger regeneration
649
650 // Queue texture update via Arena's deferred system
653
654 util::logf("Tile16 blockset refreshed and regenerated");
655 return absl::OkStatus();
656}
657
659 gfx::ScopedTimer timer("tile16_blockset_update");
660
661 if (!tile16_blockset_) {
662 return absl::FailedPreconditionError("Tile16 blockset not initialized");
663 }
664
665 if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
666 return absl::OutOfRangeError("Current tile16 ID out of range");
667 }
668
669 // Use optimized batch operations for better performance
671 // Calculate the position of this tile in the blockset bitmap
672 constexpr int kTilesPerRow =
673 8; // Standard SNES tile16 layout is 8 tiles per row
674 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
675 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
676
677 // Use dirty region tracking for efficient updates (region calculated but
678 // not used in current implementation)
679
680 // Copy pixel data from current tile to blockset bitmap using batch
681 // operations
682 for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
683 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
684 ++tile_x_offset) {
685 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
686 int dst_index =
687 (tile_y + tile_y_offset) * tile16_blockset_bmp_.width() +
688 (tile_x + tile_x_offset);
689
690 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
691 dst_index < static_cast<int>(tile16_blockset_bmp_.size())) {
692 uint8_t pixel_value = current_tile16_bmp_.data()[src_index];
693 tile16_blockset_bmp_.WriteToPixel(dst_index, pixel_value);
694 }
695 }
696 }
697
698 // Mark the blockset bitmap as modified and queue texture update
702
703 // Also update the tile16 blockset atlas if available
705 // Update the atlas with the new tile data
706 for (int tile_y_offset = 0; tile_y_offset < kTile16Size;
707 ++tile_y_offset) {
708 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
709 ++tile_x_offset) {
710 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
711 int dst_index =
712 (tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
713 (tile_x + tile_x_offset);
714
715 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
716 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
718 dst_index, current_tile16_bmp_.data()[src_index]);
719 }
720 }
721 }
722
726 }
727 }
728
729 return absl::OkStatus();
730}
731
733 // Get the current tile16 data from ROM
734 auto* tile_data = GetCurrentTile16Data();
735 if (!tile_data) {
736 return absl::FailedPreconditionError("Cannot access current tile16 data");
737 }
738
739 // Create a new 16x16 bitmap for the tile16
740 std::vector<uint8_t> tile16_pixels(kTile16PixelCount, 0);
741
742 // Process each quadrant (2x2 grid of 8x8 tiles)
743 for (int quadrant = 0; quadrant < 4; ++quadrant) {
744 gfx::TileInfo* tile_info = nullptr;
745 int quadrant_x = quadrant % 2;
746 int quadrant_y = quadrant / 2;
747
748 // Get the tile info for this quadrant
749 switch (quadrant) {
750 case 0:
751 tile_info = &tile_data->tile0_;
752 break;
753 case 1:
754 tile_info = &tile_data->tile1_;
755 break;
756 case 2:
757 tile_info = &tile_data->tile2_;
758 break;
759 case 3:
760 tile_info = &tile_data->tile3_;
761 break;
762 }
763
764 if (!tile_info)
765 continue;
766
767 // Get the tile8 ID and properties
768 int tile8_id = tile_info->id_;
769 bool x_flip = tile_info->horizontal_mirror_;
770 bool y_flip = tile_info->vertical_mirror_;
771 // Palette information stored in tile_info but applied via separate palette
772 // system
773
774 // Get the source tile8 bitmap
775 if (tile8_id >= 0 &&
776 tile8_id < static_cast<int>(current_gfx_individual_.size()) &&
777 current_gfx_individual_[tile8_id].is_active()) {
778 const auto& source_tile8 = current_gfx_individual_[tile8_id];
779
780 // Copy the 8x8 tile into the appropriate quadrant of the 16x16 tile
781 for (int ty = 0; ty < kTile8Size; ++ty) {
782 for (int tx = 0; tx < kTile8Size; ++tx) {
783 // Apply flip transformations
784 int src_x = x_flip ? (kTile8Size - 1 - tx) : tx;
785 int src_y = y_flip ? (kTile8Size - 1 - ty) : ty;
786 int src_index = src_y * kTile8Size + src_x;
787
788 // Calculate destination in tile16
789 int dst_x = (quadrant_x * kTile8Size) + tx;
790 int dst_y = (quadrant_y * kTile8Size) + ty;
791 int dst_index = dst_y * kTile16Size + dst_x;
792
793 // Copy pixel with bounds checking
794 if (src_index >= 0 &&
795 src_index < static_cast<int>(source_tile8.size()) &&
796 dst_index >= 0 && dst_index < kTile16PixelCount) {
797 uint8_t pixel = source_tile8.data()[src_index];
798 // Apply palette offset if needed
799 tile16_pixels[dst_index] = pixel;
800 }
801 }
802 }
803 }
804 }
805
806 // Update the current tile16 bitmap with the regenerated data
808
809 // Set the appropriate palette using the same system as overworld
811
812 // Queue texture creation via Arena's deferred system
815
816 util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
818 return absl::OkStatus();
819}
820
821absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
822 const gfx::Bitmap* source_tile) {
823 constexpr int kTile8Size = 8;
824 constexpr int kTile16Size = 16;
825
826 // Save undo state before making changes
827 auto now = std::chrono::steady_clock::now();
828 auto time_since_last_edit =
829 std::chrono::duration_cast<std::chrono::milliseconds>(now -
831 .count();
832
833 if (time_since_last_edit > 100) { // 100ms threshold
835 last_edit_time_ = now;
836 }
837
838 // Validate inputs
839 if (current_tile8_ < 0 ||
840 current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
841 return absl::OutOfRangeError(
842 absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
843 }
844
845 if (!current_gfx_individual_[current_tile8_].is_active()) {
846 return absl::FailedPreconditionError("Source tile8 bitmap not active");
847 }
848
850 return absl::FailedPreconditionError("Target tile16 bitmap not active");
851 }
852
853 // Determine which quadrant was clicked - handle the 8x scale factor properly
854 int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
855 int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
856
857 int start_x = quadrant_x * kTile8Size;
858 int start_y = quadrant_y * kTile8Size;
859
860 // Get source tile8 data - use provided tile if available, otherwise use
861 // current tile8
862 const gfx::Bitmap* tile_to_use =
863 source_tile ? source_tile : &current_gfx_individual_[current_tile8_];
864 if (tile_to_use->size() < 64) {
865 return absl::FailedPreconditionError("Source tile data too small");
866 }
867
868 // Copy tile8 into tile16 quadrant with proper transformations
869 for (int tile_y = 0; tile_y < kTile8Size; ++tile_y) {
870 for (int tile_x = 0; tile_x < kTile8Size; ++tile_x) {
871 // Apply flip transformations to source coordinates only if using original
872 // tile If a pre-flipped tile is provided, use direct coordinates
873 int src_x;
874 int src_y;
875 if (source_tile) {
876 // Pre-flipped tile provided, use direct coordinates
877 src_x = tile_x;
878 src_y = tile_y;
879 } else {
880 // Original tile, apply flip transformations
881 src_x = x_flip ? (kTile8Size - 1 - tile_x) : tile_x;
882 src_y = y_flip ? (kTile8Size - 1 - tile_y) : tile_y;
883 }
884 int src_index = src_y * kTile8Size + src_x;
885
886 // Calculate destination in tile16
887 int dst_x = start_x + tile_x;
888 int dst_y = start_y + tile_y;
889 int dst_index = dst_y * kTile16Size + dst_x;
890
891 // Bounds check and copy pixel
892 if (src_index >= 0 && src_index < static_cast<int>(tile_to_use->size()) &&
893 dst_index >= 0 &&
894 dst_index < static_cast<int>(current_tile16_bmp_.size())) {
895 uint8_t pixel_value = tile_to_use->data()[src_index];
896
897 // Keep original pixel values - palette selection is handled by TileInfo
898 // metadata not by modifying pixel data directly
899 current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
900 }
901 }
902 }
903
904 // Mark the bitmap as modified and queue texture update
908
909 // Update ROM data when painting to tile16
910 auto* tile_data = GetCurrentTile16Data();
911 if (tile_data) {
912 // Update the quadrant's TileInfo based on current settings
913 int quadrant_index = quadrant_x + (quadrant_y * 2);
914 if (quadrant_index >= 0 && quadrant_index < 4) {
915 // Create new TileInfo with current settings
916 gfx::TileInfo new_tile_info(static_cast<uint16_t>(current_tile8_),
919
920 // Get pointer to the correct quadrant TileInfo
921 gfx::TileInfo* quadrant_tile = nullptr;
922 switch (quadrant_index) {
923 case 0:
924 quadrant_tile = &tile_data->tile0_;
925 break;
926 case 1:
927 quadrant_tile = &tile_data->tile1_;
928 break;
929 case 2:
930 quadrant_tile = &tile_data->tile2_;
931 break;
932 case 3:
933 quadrant_tile = &tile_data->tile3_;
934 break;
935 }
936
937 if (quadrant_tile && !(*quadrant_tile == new_tile_info)) {
938 *quadrant_tile = new_tile_info;
939 // Update the tiles_info array as well
940 tile_data->tiles_info[quadrant_index] = new_tile_info;
941
943 "Updated ROM Tile16 %d, quadrant %d: Tile8=%d, Pal=%d, XFlip=%d, "
944 "YFlip=%d, Priority=%d",
947 }
948 }
949 }
950
951 // CRITICAL FIX: Don't write to ROM immediately - only update local data
952 // ROM will be updated when user explicitly clicks "Save to ROM"
953
954 // Update the blockset bitmap displayed in the editor (local preview only)
956
957 // Update live preview if enabled (but don't save to ROM)
960 }
961
962 // Track this tile as having pending changes
964
966 "Local tile16 changes made (not saved to ROM yet). Use 'Apply Changes' "
967 "to commit.");
968
969 return absl::OkStatus();
970}
971
973 static bool show_advanced_controls = false;
974 static bool show_debug_info = false;
975
976 // Modern header with improved styling
977 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 4));
978 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 4));
979
980 // Header section with better visual hierarchy
981 ImGui::BeginGroup();
982 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tile16 Editor");
983 ImGui::SameLine();
984 ImGui::TextDisabled("ID: %02X", current_tile16_);
985 ImGui::SameLine();
986 ImGui::TextDisabled("|");
987 ImGui::SameLine();
988 ImGui::TextDisabled("Palette: %d", current_palette_);
989
990 // Show pending changes indicator
991 if (has_pending_changes()) {
992 ImGui::SameLine();
993 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "| %d pending",
995 }
996
997 // Show actual palette slot for debugging
998 if (show_debug_info) {
999 ImGui::SameLine();
1000 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1001 ImGui::TextDisabled("(Slot: %d)", actual_slot);
1002 }
1003
1004 ImGui::EndGroup();
1005
1006 // Apply/Discard buttons (only shown when there are pending changes)
1007 if (has_pending_changes()) {
1008 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 340);
1009 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
1010 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1011 ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
1012 if (ImGui::Button("Apply All", ImVec2(70, 0))) {
1013 auto status = CommitAllChanges();
1014 if (!status.ok()) {
1015 util::logf("Failed to commit changes: %s", status.message().data());
1016 }
1017 }
1018 ImGui::PopStyleColor(2);
1019 if (ImGui::IsItemHovered()) {
1020 ImGui::SetTooltip("Commit all %d pending changes to ROM",
1022 }
1023
1024 ImGui::SameLine();
1025 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
1026 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1027 ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
1028 if (ImGui::Button("Discard All", ImVec2(70, 0))) {
1030 }
1031 ImGui::PopStyleColor(2);
1032 if (ImGui::IsItemHovered()) {
1033 ImGui::SetTooltip("Discard all %d pending changes",
1035 }
1036
1037 ImGui::SameLine();
1038 }
1039
1040 // Modern button styling for controls
1041 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 180);
1042 if (ImGui::Button("Debug Info", ImVec2(80, 0))) {
1043 show_debug_info = !show_debug_info;
1044 }
1045 ImGui::SameLine();
1046 if (ImGui::Button("Advanced", ImVec2(80, 0))) {
1047 show_advanced_controls = !show_advanced_controls;
1048 }
1049
1050 ImGui::PopStyleVar(2);
1051
1052 ImGui::Separator();
1053
1054 // REFACTORED: Improved 3-column layout with better space utilization
1055 if (ImGui::BeginTable("##Tile16EditLayout", 3,
1056 ImGuiTableFlags_Resizable |
1057 ImGuiTableFlags_BordersInnerV |
1058 ImGuiTableFlags_SizingStretchProp)) {
1059 ImGui::TableSetupColumn("Tile16 Blockset",
1060 ImGuiTableColumnFlags_WidthStretch, 0.35f);
1061 ImGui::TableSetupColumn("Tile8 Source", ImGuiTableColumnFlags_WidthStretch,
1062 0.35f);
1063 ImGui::TableSetupColumn("Editor & Controls",
1064 ImGuiTableColumnFlags_WidthStretch, 0.30f);
1065
1066 ImGui::TableHeadersRow();
1067 ImGui::TableNextRow();
1068
1069 // ========== COLUMN 1: Tile16 Blockset ==========
1070 ImGui::TableNextColumn();
1071 ImGui::BeginGroup();
1072
1073 // Navigation header with tile info
1074 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile16 Blockset");
1075 ImGui::SameLine();
1076
1077 // Show current tile and total tiles
1078 int total_tiles =
1079 tile16_blockset_ ? static_cast<int>(tile16_blockset_->atlas.size()) : 0;
1080 if (total_tiles == 0)
1081 total_tiles = zelda3::kNumTile16Individual;
1082 ImGui::TextDisabled("(%d / %d)", current_tile16_, total_tiles);
1083
1084 // Navigation controls row
1085 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
1086
1087 // Jump to Tile ID input - live navigation as user types
1088 ImGui::SetNextItemWidth(80);
1089 if (ImGui::InputInt("##JumpToTile", &jump_to_tile_id_, 0, 0)) {
1090 // Clamp to valid range
1091 jump_to_tile_id_ = std::clamp(jump_to_tile_id_, 0, total_tiles - 1);
1094 scroll_to_current_ = true;
1095 }
1096 }
1097 if (ImGui::IsItemHovered()) {
1098 ImGui::SetTooltip("Tile ID (0-%d) - navigates as you type",
1099 total_tiles - 1);
1100 }
1101
1102 ImGui::SameLine();
1103 ImGui::TextDisabled("|");
1104 ImGui::SameLine();
1105
1106 // Page navigation
1107 int total_pages = (total_tiles + kTilesPerPage - 1) / kTilesPerPage;
1109
1110 if (ImGui::Button("<<")) {
1112 scroll_to_current_ = true;
1113 }
1114 if (ImGui::IsItemHovered())
1115 ImGui::SetTooltip("First page");
1116
1117 ImGui::SameLine();
1118 if (ImGui::Button("<")) {
1119 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
1120 RequestTileSwitch(new_tile);
1121 scroll_to_current_ = true;
1122 }
1123 if (ImGui::IsItemHovered())
1124 ImGui::SetTooltip("Previous page (PageUp)");
1125
1126 ImGui::SameLine();
1127 ImGui::TextDisabled("Page %d/%d", current_page_ + 1, total_pages);
1128
1129 ImGui::SameLine();
1130 if (ImGui::Button(">")) {
1131 int new_tile = std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
1132 RequestTileSwitch(new_tile);
1133 scroll_to_current_ = true;
1134 }
1135 if (ImGui::IsItemHovered())
1136 ImGui::SetTooltip("Next page (PageDown)");
1137
1138 ImGui::SameLine();
1139 if (ImGui::Button(">>")) {
1140 RequestTileSwitch(total_tiles - 1);
1141 scroll_to_current_ = true;
1142 }
1143 if (ImGui::IsItemHovered())
1144 ImGui::SetTooltip("Last page");
1145
1146 // Display current tile info (sheet and palette)
1147 ImGui::SameLine();
1148 ImGui::TextDisabled("|");
1149 ImGui::SameLine();
1150 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1151 ImGui::Text("Sheet: %d | Palette: %d", sheet_idx, current_palette_);
1152
1153 // Handle keyboard shortcuts for page navigation
1154 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
1155 if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
1156 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
1157 RequestTileSwitch(new_tile);
1158 scroll_to_current_ = true;
1159 }
1160 if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
1161 int new_tile =
1162 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
1163 RequestTileSwitch(new_tile);
1164 scroll_to_current_ = true;
1165 }
1166 if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
1168 scroll_to_current_ = true;
1169 }
1170 if (ImGui::IsKeyPressed(ImGuiKey_End)) {
1171 RequestTileSwitch(total_tiles - 1);
1172 scroll_to_current_ = true;
1173 }
1174
1175 // Arrow keys for single-tile navigation (when Ctrl not held)
1176 if (!ImGui::GetIO().KeyCtrl) {
1177 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
1178 if (current_tile16_ > 0) {
1180 scroll_to_current_ = true;
1181 }
1182 }
1183 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
1184 if (current_tile16_ < total_tiles - 1) {
1186 scroll_to_current_ = true;
1187 }
1188 }
1189 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
1192 scroll_to_current_ = true;
1193 }
1194 }
1195 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
1196 if (current_tile16_ + kTilesPerRow < total_tiles) {
1198 scroll_to_current_ = true;
1199 }
1200 }
1201 }
1202 }
1203
1204 ImGui::PopStyleVar();
1205
1206 // Blockset canvas with scrolling
1207 if (BeginChild("##BlocksetScrollable",
1208 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
1209 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1210 // Handle scroll-to-current request
1211 if (scroll_to_current_) {
1212 int tile_row = current_tile16_ / kTilesPerRow;
1213 float tile_y = tile_row * 32.0f * blockset_canvas_.GetGlobalScale();
1214 ImGui::SetScrollY(tile_y);
1215 scroll_to_current_ = false;
1216 }
1217
1218 // Configure canvas frame options for blockset
1219 gui::CanvasFrameOptions blockset_frame_opts;
1220 blockset_frame_opts.draw_grid = true;
1221 blockset_frame_opts.grid_step = 32.0f;
1222 blockset_frame_opts.draw_context_menu = true;
1223 blockset_frame_opts.draw_overlay = true;
1224 blockset_frame_opts.render_popups = true;
1225 blockset_frame_opts.use_child_window = false;
1226
1227 auto blockset_rt =
1228 gui::BeginCanvas(blockset_canvas_, blockset_frame_opts);
1229
1230 // Handle tile selection from blockset
1231 bool tile_selected = false;
1233
1234 if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
1236 tile_selected = true;
1237 }
1238
1239 if (tile_selected) {
1240 const ImGuiIO& io = ImGui::GetIO();
1241 ImVec2 canvas_pos = blockset_canvas_.zero_point();
1242 ImVec2 mouse_pos =
1243 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1244
1245 int grid_x = static_cast<int>(mouse_pos.x /
1247 int grid_y = static_cast<int>(mouse_pos.y /
1249 int selected_tile = grid_x + grid_y * 8;
1250
1251 if (selected_tile != current_tile16_ && selected_tile >= 0) {
1252 // Use RequestTileSwitch to handle pending changes confirmation
1253 RequestTileSwitch(selected_tile);
1254 util::logf("Selected Tile16 from blockset: %d", selected_tile);
1255 }
1256 }
1257
1259
1260 gui::EndCanvas(blockset_canvas_, blockset_rt, blockset_frame_opts);
1261 }
1262 EndChild();
1263 ImGui::EndGroup();
1264
1265 // ========== COLUMN 2: Tile8 Source ==========
1266 ImGui::TableNextColumn();
1267 ImGui::BeginGroup();
1268
1269 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile8 Source");
1270
1272
1273 // Scrollable tile8 source
1274 if (BeginChild("##Tile8SourceScrollable", ImVec2(0, 0), true,
1275 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1276 // Configure canvas frame options for tile8 source
1277 gui::CanvasFrameOptions tile8_frame_opts;
1278 tile8_frame_opts.draw_grid = true;
1279 tile8_frame_opts.grid_step = 32.0f; // Tile8 grid (8px * 4 scale)
1280 tile8_frame_opts.draw_context_menu = true;
1281 tile8_frame_opts.draw_overlay = true;
1282 tile8_frame_opts.render_popups = true;
1283 tile8_frame_opts.use_child_window = false;
1284
1285 auto tile8_rt = gui::BeginCanvas(tile8_source_canvas_, tile8_frame_opts);
1286
1287 // Tile8 selection with improved feedback
1288 bool tile8_selected = false;
1290
1291 // Check for clicks properly
1292 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
1293 tile8_selected = true;
1294 }
1295
1296 if (tile8_selected) {
1297 const ImGuiIO& io = ImGui::GetIO();
1298 ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
1299 ImVec2 mouse_pos =
1300 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1301
1302 // Account for dynamic zoom when calculating tile position
1303 int tile_x = static_cast<int>(
1304 mouse_pos.x / (8 * kTile8DisplayScale)); // 8 pixel tile * scale
1305 int tile_y = static_cast<int>(mouse_pos.y / (8 * kTile8DisplayScale));
1306
1307 // Calculate tiles per row based on bitmap width
1308 int tiles_per_row = current_gfx_bmp_.width() / 8;
1309 int new_tile8 = tile_x + (tile_y * tiles_per_row);
1310
1311 if (new_tile8 != current_tile8_ && new_tile8 >= 0 &&
1312 new_tile8 < static_cast<int>(current_gfx_individual_.size()) &&
1313 current_gfx_individual_[new_tile8].is_active()) {
1314 current_tile8_ = new_tile8;
1316 util::logf("Selected Tile8: %d", current_tile8_);
1317 }
1318 }
1319
1322
1323 gui::EndCanvas(tile8_source_canvas_, tile8_rt, tile8_frame_opts);
1324 }
1325 EndChild();
1326 ImGui::EndGroup();
1327
1328 // ========== COLUMN 3: Tile16 Editor + Controls ==========
1329 TableNextColumn();
1330 ImGui::BeginGroup();
1331
1332 // Fixed size container to prevent canvas expansion
1333 if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
1334 ImGuiWindowFlags_NoScrollbar |
1335 ImGuiWindowFlags_NoScrollWithMouse)) {
1336 // Configure canvas frame options for tile16 editor
1337 gui::CanvasFrameOptions tile16_edit_frame_opts;
1338 tile16_edit_frame_opts.canvas_size = ImVec2(64, 64);
1339 tile16_edit_frame_opts.draw_grid = true;
1340 tile16_edit_frame_opts.grid_step = 8.0f; // 8x8 grid for tile8 placement
1341 tile16_edit_frame_opts.draw_context_menu = true;
1342 tile16_edit_frame_opts.draw_overlay = true;
1343 tile16_edit_frame_opts.render_popups = true;
1344 tile16_edit_frame_opts.use_child_window = false;
1345
1346 auto tile16_edit_rt =
1347 gui::BeginCanvas(tile16_edit_canvas_, tile16_edit_frame_opts);
1348
1349 // Draw current tile16 bitmap with dynamic zoom
1352 }
1353
1354 // Handle tile8 painting with improved hover preview
1355 if (current_tile8_ >= 0 &&
1356 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
1358 // Create a display tile that shows the current palette selection
1360 tile8_preview_bmp_.Create(8, 8, 8,
1361 std::vector<uint8_t>(kTile8PixelCount, 0));
1362 }
1363
1364 // Get the original pixel data (already has sheet offsets from
1365 // ProcessGraphicsBuffer)
1368
1369 // Apply the correct sheet-aware palette slice for the preview
1370 const gfx::SnesPalette* display_palette = nullptr;
1371 if (overworld_palette_.size() >= 256) {
1372 display_palette = &overworld_palette_;
1373 } else if (palette_.size() >= 256) {
1374 display_palette = &palette_;
1375 } else {
1376 display_palette = &current_gfx_individual_[current_tile8_].palette();
1377 }
1378
1379 if (display_palette && !display_palette->empty()) {
1381 // Calculate palette slot for the selected tile8
1382 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1383 int palette_slot =
1385
1386 // SNES palette offset fix: pixel value N maps to sub-palette color N
1387 // Color 0 is handled by SetPaletteWithTransparent (transparent)
1388 // Colors 1-15 need to come from palette[slot+1] through palette[slot+15]
1389 if (palette_slot >= 0 && static_cast<size_t>(palette_slot + 16) <=
1390 display_palette->size()) {
1392 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
1393 } else {
1395 15);
1396 }
1397 } else {
1398 tile8_preview_bmp_.SetPalette(*display_palette);
1399 }
1400 }
1401
1402 // Apply flips if needed
1403 if (x_flip || y_flip) {
1404 auto& data = tile8_preview_bmp_.mutable_data();
1405
1406 if (x_flip) {
1407 for (int y = 0; y < 8; ++y) {
1408 for (int x = 0; x < 4; ++x) {
1409 std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
1410 }
1411 }
1412 }
1413
1414 if (y_flip) {
1415 for (int y = 0; y < 4; ++y) {
1416 for (int x = 0; x < 8; ++x) {
1417 std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
1418 }
1419 }
1420 }
1421 }
1422
1423 // Push pixel changes to the existing surface before queuing texture work
1425
1426 // Queue texture creation/update on the persistent preview bitmap to
1427 // avoid dangling stack pointers in the arena queue
1428 const auto preview_command =
1432 gfx::Arena::Get().QueueTextureCommand(preview_command,
1434
1435 // CRITICAL FIX: Handle tile painting with simple click instead of
1436 // click+drag Draw the preview first
1439
1440 // Check for simple click to paint tile8 to tile16
1441 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
1442 const ImGuiIO& io = ImGui::GetIO();
1443 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1444 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1445 io.MousePos.y - canvas_pos.y);
1446
1447 // Convert canvas coordinates to tile16 coordinates
1448 // Account for bitmap offset (2,2) and scale (4x)
1449 constexpr float kBitmapOffset = 2.0f;
1450 constexpr float kBitmapScale = 4.0f;
1451 int tile_x =
1452 static_cast<int>((mouse_pos.x - kBitmapOffset) / kBitmapScale);
1453 int tile_y =
1454 static_cast<int>((mouse_pos.y - kBitmapOffset) / kBitmapScale);
1455
1456 // Clamp to valid range (0-15 for 16x16 tile)
1457 tile_x = std::max(0, std::min(15, tile_x));
1458 tile_y = std::max(0, std::min(15, tile_y));
1459
1460 util::logf("Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d)",
1461 mouse_pos.x, mouse_pos.y, tile_x, tile_y);
1462
1463 // Pass nullptr to let DrawToCurrentTile16 handle flipping and store
1464 // correct TileInfo metadata. The preview bitmap is pre-flipped for
1465 // display only.
1466 RETURN_IF_ERROR(DrawToCurrentTile16(ImVec2(tile_x, tile_y), nullptr));
1467 }
1468
1469 // Right-click to pick tile8 from tile16
1470 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1471 const ImGuiIO& io = ImGui::GetIO();
1472 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1473 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1474 io.MousePos.y - canvas_pos.y);
1475
1476 // Convert canvas coordinates to tile16 coordinates
1477 // Account for bitmap offset (2,2) and scale (4x)
1478 constexpr float kBitmapOffset = 2.0f;
1479 constexpr float kBitmapScale = 4.0f;
1480 int tile_x =
1481 static_cast<int>((mouse_pos.x - kBitmapOffset) / kBitmapScale);
1482 int tile_y =
1483 static_cast<int>((mouse_pos.y - kBitmapOffset) / kBitmapScale);
1484
1485 // Clamp to valid range (0-15 for 16x16 tile)
1486 tile_x = std::max(0, std::min(15, tile_x));
1487 tile_y = std::max(0, std::min(15, tile_y));
1488
1489 RETURN_IF_ERROR(PickTile8FromTile16(ImVec2(tile_x, tile_y)));
1490 util::logf("Right-clicked to pick tile8 from tile16 at (%d, %d)",
1491 tile_x, tile_y);
1492 }
1493 }
1494
1495 gui::EndCanvas(tile16_edit_canvas_, tile16_edit_rt,
1496 tile16_edit_frame_opts);
1497 }
1498 ImGui::EndChild();
1499
1500 Separator();
1501
1502 // === Compact Controls Section ===
1503
1504 // Tile8 info and preview
1505 if (current_tile8_ >= 0 &&
1506 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
1508 Text("Tile8: %02X", current_tile8_);
1509 SameLine();
1510 auto* tile8_texture = current_gfx_individual_[current_tile8_].texture();
1511 if (tile8_texture) {
1512 ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
1513 }
1514
1515 // Show encoded palette row indicator
1516 // This shows which palette row the tile is encoded to use in the ROM
1517 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1518 int encoded_row = -1;
1519
1520 // Determine encoded row based on sheet and ProcessGraphicsBuffer behavior
1521 // Sheets 0, 3, 4, 5 have 0x88 added (row 8-9)
1522 // Other sheets have raw values (row 0)
1523 switch (sheet_idx) {
1524 case 0:
1525 case 3:
1526 case 4:
1527 case 5:
1528 encoded_row = 8; // 0x88 offset = row 8
1529 break;
1530 default:
1531 encoded_row = 0; // Raw values = row 0
1532 break;
1533 }
1534
1535 // Visual indicator showing sheet and encoded row
1536 ImGui::SameLine();
1537 ImGui::TextDisabled("S%d", sheet_idx);
1538 if (ImGui::IsItemHovered()) {
1539 ImGui::BeginTooltip();
1540 ImGui::Text("Sheet: %d", sheet_idx);
1541 ImGui::Text("Encoded Palette Row: %d", encoded_row);
1542 ImGui::Separator();
1543 ImGui::TextWrapped(
1544 "Graphics sheets have different palette encodings:\n"
1545 "- Sheets 0,3,4,5: Row 8 (offset 0x88)\n"
1546 "- Sheets 1,2,6,7: Row 0 (raw)");
1547 ImGui::EndTooltip();
1548 }
1549 }
1550
1551 // Tile8 transform options in compact form
1552 Checkbox("X Flip", &x_flip);
1553 SameLine();
1554 Checkbox("Y Flip", &y_flip);
1555 SameLine();
1556 Checkbox("Priority", &priority_tile);
1557
1558 Separator();
1559
1560 // Palette selector - more compact
1561 Text("Palette:");
1562 if (show_debug_info) {
1563 SameLine();
1564 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1565 ImGui::TextDisabled("(Slot %d)", actual_slot);
1566 }
1567
1568 // Compact palette grid
1569 ImGui::BeginGroup();
1570 float available_width = ImGui::GetContentRegionAvail().x;
1571 float button_size = std::min(32.0f, (available_width - 16.0f) / 4.0f);
1572
1573 for (int row = 0; row < 2; ++row) {
1574 for (int col = 0; col < 4; ++col) {
1575 if (col > 0)
1576 ImGui::SameLine();
1577
1578 int i = row * 4 + col;
1579 bool is_current = (current_palette_ == i);
1580
1581 // Modern button styling with better visual hierarchy
1582 ImGui::PushID(i);
1583
1584 if (is_current) {
1585 ImGui::PushStyleColor(ImGuiCol_Button,
1586 ImVec4(0.2f, 0.7f, 0.3f, 1.0f));
1587 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1588 ImVec4(0.3f, 0.8f, 0.4f, 1.0f));
1589 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1590 ImVec4(0.1f, 0.6f, 0.2f, 1.0f));
1591 } else {
1592 ImGui::PushStyleColor(ImGuiCol_Button,
1593 ImVec4(0.3f, 0.3f, 0.35f, 1.0f));
1594 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1595 ImVec4(0.4f, 0.4f, 0.45f, 1.0f));
1596 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1597 ImVec4(0.25f, 0.25f, 0.3f, 1.0f));
1598 }
1599
1600 // Add border for better definition
1601 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
1602 ImGui::PushStyleColor(ImGuiCol_Border,
1603 is_current ? ImVec4(0.4f, 0.9f, 0.5f, 1.0f)
1604 : ImVec4(0.5f, 0.5f, 0.5f, 0.3f));
1605
1606 if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
1607 ImVec2(button_size, button_size))) {
1608 if (current_palette_ != i) {
1609 current_palette_ = i;
1610 auto status = RefreshAllPalettes();
1611 if (!status.ok()) {
1612 util::logf("Failed to refresh palettes: %s",
1613 status.message().data());
1614 } else {
1615 util::logf("Palette successfully changed to %d",
1617 }
1618 }
1619 }
1620
1621 ImGui::PopStyleColor(4); // 3 button colors + 1 border color
1622 ImGui::PopStyleVar(1); // border size
1623 ImGui::PopID();
1624
1625 // Simplified tooltip
1626 if (ImGui::IsItemHovered()) {
1627 ImGui::BeginTooltip();
1628 if (show_debug_info) {
1629 ImGui::Text("Palette %d → Slots:", i);
1630 ImGui::Text(" S0,3,4: %d", GetActualPaletteSlot(i, 0));
1631 ImGui::Text(" S1,2: %d", GetActualPaletteSlot(i, 1));
1632 ImGui::Text(" S5,6: %d", GetActualPaletteSlot(i, 5));
1633 ImGui::Text(" S7: %d", GetActualPaletteSlot(i, 7));
1634 } else {
1635 ImGui::Text("Palette %d", i);
1636 if (is_current) {
1637 ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Active");
1638 }
1639 }
1640 ImGui::EndTooltip();
1641 }
1642 }
1643 }
1644 ImGui::EndGroup();
1645
1646 Separator();
1647
1648 // Compact action buttons
1649 if (Button("Clear", ImVec2(-1, 0))) {
1651 }
1652
1653 if (Button("Copy", ImVec2(-1, 0))) {
1655 }
1656
1657 if (Button("Paste", ImVec2(-1, 0))) {
1659 }
1660
1661 Separator();
1662
1663 // Save/Discard - full width buttons
1664 if (Button("Save Changes", ImVec2(-1, 0))) {
1666 }
1667 HOVER_HINT("Apply changes to overworld and regenerate blockset");
1668
1669 if (Button("Discard Changes", ImVec2(-1, 0))) {
1671 }
1672 HOVER_HINT("Reload tile16 from ROM, discarding local changes");
1673
1674 Separator();
1675
1676 bool can_undo = !undo_stack_.empty();
1677 if (!can_undo)
1678 BeginDisabled();
1679 if (Button("Undo", ImVec2(-1, 0))) {
1681 }
1682 if (!can_undo)
1683 EndDisabled();
1684
1685 // Advanced controls (collapsible)
1686 if (show_advanced_controls) {
1687 Separator();
1688 Text("Advanced:");
1689
1690 if (Button("Palette Settings", ImVec2(-1, 0))) {
1692 }
1693
1694 if (Button("Analyze Data", ImVec2(-1, 0))) {
1696 }
1697 HOVER_HINT("Analyze tile8 source data format and palette state");
1698
1699 if (Button("Manual Edit", ImVec2(-1, 0))) {
1700 ImGui::OpenPopup("ManualTile8Editor");
1701 }
1702
1703 if (Button("Refresh Blockset", ImVec2(-1, 0))) {
1705 }
1706
1707 // Scratch space in compact form
1708 Text("Scratch:");
1710
1711 // Manual tile8 editor popup
1713 }
1714
1715 // Compact debug information panel
1716 if (show_debug_info) {
1717 Separator();
1718 Text("Debug:");
1719 ImGui::TextDisabled("T16:%02X T8:%d Pal:%d", current_tile16_,
1721
1722 if (current_tile8_ >= 0) {
1723 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1724 int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
1725 ImGui::TextDisabled("Sheet:%d Slot:%d", sheet_index, actual_slot);
1726 }
1727
1728 // Compact palette mapping table
1729 if (ImGui::CollapsingHeader("Palette Map",
1730 ImGuiTreeNodeFlags_DefaultOpen)) {
1731 ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
1732 if (ImGui::BeginTable("##PalMap", 3,
1733 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1734 ImGuiTableFlags_SizingFixedFit)) {
1735 ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
1736 ImGui::TableSetupColumn("S0,3-4", ImGuiTableColumnFlags_WidthFixed,
1737 50);
1738 ImGui::TableSetupColumn("S1-2", ImGuiTableColumnFlags_WidthFixed, 50);
1739 ImGui::TableHeadersRow();
1740
1741 for (int i = 0; i < 8; ++i) {
1742 ImGui::TableNextRow();
1743 ImGui::TableNextColumn();
1744 ImGui::Text("%d", i);
1745 ImGui::TableNextColumn();
1746 ImGui::Text("%d", GetActualPaletteSlot(i, 0));
1747 ImGui::TableNextColumn();
1748 ImGui::Text("%d", GetActualPaletteSlot(i, 1));
1749 }
1750 ImGui::EndTable();
1751 }
1752 ImGui::EndChild();
1753 }
1754
1755 // Color preview - compact
1756 if (ImGui::CollapsingHeader("Colors")) {
1757 if (overworld_palette_.size() >= 256) {
1758 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1759 ImGui::Text("Slot %d:", actual_slot);
1760
1761 for (int i = 0;
1762 i < 8 &&
1763 (actual_slot + i) < static_cast<int>(overworld_palette_.size());
1764 ++i) {
1765 int color_index = actual_slot + i;
1766 auto color = overworld_palette_[color_index];
1767 ImVec4 display_color = color.rgb();
1768
1769 ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
1770 display_color, ImGuiColorEditFlags_NoTooltip,
1771 ImVec2(20, 20));
1772 if (ImGui::IsItemHovered()) {
1773 ImGui::SetTooltip("%d:0x%04X", color_index, color.snes());
1774 }
1775
1776 if ((i + 1) % 4 != 0)
1777 ImGui::SameLine();
1778 }
1779 }
1780 }
1781 }
1782
1783 ImGui::EndGroup();
1784 EndTable();
1785 }
1786
1787 // Draw palette settings and canvas popups
1789
1790 // Show canvas popup windows if opened from context menu
1797
1798 return absl::OkStatus();
1799}
1800
1802 if (!current_gfx_bmp_.is_active() || current_gfx_bmp_.data() == nullptr) {
1803 return absl::FailedPreconditionError(
1804 "Current graphics bitmap not initialized");
1805 }
1806
1808
1809 // Calculate how many 8x8 tiles we can fit based on the current graphics
1810 // bitmap size SNES graphics are typically 128 pixels wide (16 tiles of 8
1811 // pixels each)
1812 const int tiles_per_row = current_gfx_bmp_.width() / 8;
1813 const int total_rows = current_gfx_bmp_.height() / 8;
1814 const int total_tiles = tiles_per_row * total_rows;
1815
1816 current_gfx_individual_.reserve(total_tiles);
1817
1818 // Extract individual 8x8 tiles from the graphics bitmap
1819 for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
1820 for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
1821 std::vector<uint8_t> tile_data(64); // 8x8 = 64 pixels
1822
1823 // Extract tile data from the main graphics bitmap.
1824 // Preserve encoded palette offsets unless normalization is enabled.
1825 for (int py = 0; py < 8; ++py) {
1826 for (int px = 0; px < 8; ++px) {
1827 int src_x = tile_x * 8 + px;
1828 int src_y = tile_y * 8 + py;
1829 int src_index = src_y * current_gfx_bmp_.width() + src_x;
1830 int dst_index = py * 8 + px;
1831
1832 if (src_index < static_cast<int>(current_gfx_bmp_.size()) &&
1833 dst_index < 64) {
1834 uint8_t pixel_value = current_gfx_bmp_.data()[src_index];
1835
1837 pixel_value &= palette_normalization_mask_;
1838 }
1839
1840 tile_data[dst_index] = pixel_value;
1841 }
1842 }
1843 }
1844
1845 // Create the individual tile bitmap
1846 current_gfx_individual_.emplace_back();
1847 auto& tile_bitmap = current_gfx_individual_.back();
1848
1849 try {
1850 tile_bitmap.Create(8, 8, 8, tile_data);
1851
1852 // Set default palette using the same system as overworld
1853 if (overworld_palette_.size() >= 256) {
1854 // Use complete 256-color palette (same as overworld system)
1855 // The pixel data already contains correct color indices for the
1856 // 256-color palette
1857 tile_bitmap.SetPalette(overworld_palette_);
1858 } else if (game_data() &&
1859 game_data()->palette_groups.overworld_main.size() > 0) {
1860 // Fallback to GameData palette
1861 tile_bitmap.SetPalette(game_data()->palette_groups.overworld_main[0]);
1862 }
1863 // Queue texture creation via Arena's deferred system
1866 } catch (const std::exception& e) {
1867 util::logf("Error creating tile at (%d,%d): %s", tile_x, tile_y,
1868 e.what());
1869 // Create an empty bitmap as fallback
1870 tile_bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
1871 }
1872 }
1873 }
1874
1875 // Apply current palette settings to all tiles
1876 if (rom_) {
1878 }
1879
1880 // Ensure canvas scroll size matches the full tilesheet at preview scale
1884
1885 util::logf("Loaded %zu individual tile8 graphics",
1887 return absl::OkStatus();
1888}
1889
1890absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
1891 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1892 return absl::OutOfRangeError(
1893 absl::StrFormat("Invalid tile16 id: %d", tile_id));
1894 }
1895
1896 if (!tile16_blockset_) {
1897 return absl::FailedPreconditionError("Tile16 blockset not initialized");
1898 }
1899
1900 current_tile16_ = tile_id;
1901 jump_to_tile_id_ = tile_id; // Sync input field with current tile
1902
1903 // Initialize the instance variable with current ROM data
1904 auto tile_result = rom_->ReadTile16(current_tile16_, zelda3::kTile16Ptr);
1905 if (tile_result.ok()) {
1906 current_tile16_data_ = tile_result.value();
1907 }
1908
1909 // Extract tile data using the same method as GetTilemapData
1910 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
1911
1912 if (tile_data.empty()) {
1913 // If GetTilemapData fails, manually extract from the atlas
1914 const int kTilesPerRow = 8; // Standard tile16 blockset layout
1915 int tile_x = (tile_id % kTilesPerRow) * kTile16Size;
1916 int tile_y = (tile_id / kTilesPerRow) * kTile16Size;
1917
1918 tile_data.resize(kTile16PixelCount);
1919
1920 // Manual extraction - preserve encoded palette offsets unless
1921 // normalization is enabled.
1922 for (int ty = 0; ty < kTile16Size; ty++) {
1923 for (int tx = 0; tx < kTile16Size; tx++) {
1924 int pixel_x = tile_x + tx;
1925 int pixel_y = tile_y + ty;
1926 int src_index = (pixel_y * tile16_blockset_->atlas.width()) + pixel_x;
1927 int dst_index = ty * kTile16Size + tx;
1928
1929 if (src_index < static_cast<int>(tile16_blockset_->atlas.size()) &&
1930 dst_index < static_cast<int>(tile_data.size())) {
1931 uint8_t pixel_value = tile16_blockset_->atlas.data()[src_index];
1933 pixel_value &= palette_normalization_mask_;
1934 }
1935 tile_data[dst_index] = pixel_value;
1936 }
1937 }
1938 }
1939 } else {
1940 // Normalize the extracted data only if requested.
1941 for (auto& pixel : tile_data) {
1944 }
1945 }
1946 }
1947
1948 // Create the bitmap with the extracted data
1950
1951 // Apply palette based on whether pixel values retain CGRAM row offsets.
1952 gfx::SnesPalette display_palette;
1953 if (overworld_palette_.size() >= 256) {
1954 display_palette = overworld_palette_;
1955 } else if (palette_.size() >= 256) {
1956 display_palette = palette_;
1957 } else if (game_data() &&
1959 display_palette = game_data()->palette_groups.overworld_main[0];
1960 }
1961
1962 // Validate palette before attempting to use it.
1963 if (!display_palette.empty()) {
1965 const int palette_slot = GetActualPaletteSlotForCurrentTile16();
1966 // SNES palette offset fix: pixel value N maps to sub-palette color N
1967 // Add 1 to skip the transparent color slot (color 0 of each sub-palette)
1968 size_t palette_offset =
1969 palette_slot >= 0 ? static_cast<size_t>(palette_slot + 1) : 1;
1970
1971 // Ensure the palette offset is within bounds
1972 // SNES 4BPP uses 16 colors total (transparent + 15)
1973 if (palette_offset + 15 <= display_palette.size()) {
1974 // Apply the correct sub-palette with transparency
1976 palette_offset, 15);
1977 } else {
1978 // Fallback: use offset 1 if calculated offset exceeds palette size
1979 util::logf(
1980 "Warning: palette offset %zu exceeds palette size %zu, using "
1981 "offset 1",
1982 palette_offset, display_palette.size());
1983 current_tile16_bmp_.SetPaletteWithTransparent(display_palette, 1, 15);
1984 }
1985 } else {
1986 current_tile16_bmp_.SetPalette(display_palette);
1987 }
1988 } else {
1989 util::logf(
1990 "Warning: No valid palette available for Tile16 %d, skipping palette "
1991 "setup",
1992 tile_id);
1993 }
1994
1995 // Only queue texture if the bitmap has a valid surface
1999 util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
2000 } else {
2001 util::logf(
2002 "Warning: SetCurrentTile: bitmap not ready for tile %d (active=%d, "
2003 "surface=%p)",
2004 tile_id, current_tile16_bmp_.is_active(),
2006 }
2007
2008 return absl::OkStatus();
2009}
2010
2011void Tile16Editor::RequestTileSwitch(int target_tile_id) {
2012 // Validate that the tile16 editor is properly initialized
2013 if (!tile16_blockset_ || !rom_) {
2014 util::logf(
2015 "RequestTileSwitch: Editor not initialized (blockset=%p, rom=%p)",
2017 return;
2018 }
2019
2020 // Validate target tile ID
2021 if (target_tile_id < 0 || target_tile_id >= zelda3::kNumTile16Individual) {
2022 util::logf("RequestTileSwitch: Invalid target tile ID %d", target_tile_id);
2023 return;
2024 }
2025
2026 // Check if we're already on this tile
2027 if (target_tile_id == current_tile16_) {
2028 return;
2029 }
2030
2031 // Check if current tile has pending changes
2033 // Store target and show dialog
2034 pending_tile_switch_target_ = target_tile_id;
2036 util::logf("Tile %d has pending changes, showing confirmation dialog",
2038 } else {
2039 // No pending changes, switch directly
2040 auto status = SetCurrentTile(target_tile_id);
2041 if (!status.ok()) {
2042 util::logf("Failed to switch to tile %d: %s", target_tile_id,
2043 status.message().data());
2044 }
2045 }
2046}
2047
2048absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
2049 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
2050 return absl::InvalidArgumentError("Invalid tile ID");
2051 }
2052
2053 // CRITICAL FIX: Extract tile data directly from atlas instead of using
2054 // problematic tile cache
2055 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
2056 if (!tile_data.empty()) {
2057 clipboard_tile16_.Create(16, 16, 8, tile_data);
2059 }
2060 // Queue texture creation via Arena's deferred system
2063
2064 clipboard_has_data_ = true;
2065 return absl::OkStatus();
2066}
2067
2069 if (!clipboard_has_data_) {
2070 return absl::FailedPreconditionError("Clipboard is empty");
2071 }
2072
2073 // Copy the clipboard data to the current tile16
2076 // Queue texture creation via Arena's deferred system
2079
2080 return absl::OkStatus();
2081}
2082
2084 if (slot < 0 || slot >= 4) {
2085 return absl::InvalidArgumentError("Invalid scratch space slot");
2086 }
2087
2088 // Create a copy of the current tile16 bitmap
2089 scratch_space_[slot].Create(16, 16, 8, current_tile16_bmp_.vector());
2090 scratch_space_[slot].SetPalette(current_tile16_bmp_.palette());
2091 // Queue texture creation via Arena's deferred system
2093 &scratch_space_[slot]);
2094
2095 scratch_space_used_[slot] = true;
2096 return absl::OkStatus();
2097}
2098
2100 if (slot < 0 || slot >= 4) {
2101 return absl::InvalidArgumentError("Invalid scratch space slot");
2102 }
2103
2104 if (!scratch_space_used_[slot]) {
2105 return absl::FailedPreconditionError("Scratch space slot is empty");
2106 }
2107
2108 // Copy the scratch space data to the current tile16
2109 current_tile16_bmp_.Create(16, 16, 8, scratch_space_[slot].vector());
2111 // Queue texture creation via Arena's deferred system
2114
2115 return absl::OkStatus();
2116}
2117
2118absl::Status Tile16Editor::ClearScratchSpace(int slot) {
2119 if (slot < 0 || slot >= 4) {
2120 return absl::InvalidArgumentError("Invalid scratch space slot");
2121 }
2122
2123 scratch_space_used_[slot] = false;
2124 return absl::OkStatus();
2125}
2126
2127// Advanced editing features
2130 return absl::FailedPreconditionError("No active tile16 to flip");
2131 }
2132
2133 SaveUndoState();
2134
2135 // Create a temporary bitmap for the flipped result
2136 gfx::Bitmap flipped_bitmap;
2137 flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
2138
2139 // Flip horizontally by copying pixels in reverse x order
2140 for (int y = 0; y < 16; ++y) {
2141 for (int x = 0; x < 16; ++x) {
2142 int src_index = y * 16 + x;
2143 int dst_index = y * 16 + (15 - x);
2144 if (src_index < current_tile16_bmp_.size() &&
2145 dst_index < flipped_bitmap.size()) {
2146 flipped_bitmap.WriteToPixel(dst_index,
2147 current_tile16_bmp_.data()[src_index]);
2148 }
2149 }
2150 }
2151
2152 // Copy the flipped result back
2153 current_tile16_bmp_ = std::move(flipped_bitmap);
2155
2156 // Track this tile as having pending changes
2158
2159 return absl::OkStatus();
2160}
2161
2164 return absl::FailedPreconditionError("No active tile16 to flip");
2165 }
2166
2167 SaveUndoState();
2168
2169 // Create a temporary bitmap for the flipped result
2170 gfx::Bitmap flipped_bitmap;
2171 flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
2172
2173 // Flip vertically by copying pixels in reverse y order
2174 for (int y = 0; y < 16; ++y) {
2175 for (int x = 0; x < 16; ++x) {
2176 int src_index = y * 16 + x;
2177 int dst_index = (15 - y) * 16 + x;
2178 if (src_index < current_tile16_bmp_.size() &&
2179 dst_index < flipped_bitmap.size()) {
2180 flipped_bitmap.WriteToPixel(dst_index,
2181 current_tile16_bmp_.data()[src_index]);
2182 }
2183 }
2184 }
2185
2186 // Copy the flipped result back
2187 current_tile16_bmp_ = std::move(flipped_bitmap);
2189
2190 // Track this tile as having pending changes
2192
2193 return absl::OkStatus();
2194}
2195
2198 return absl::FailedPreconditionError("No active tile16 to rotate");
2199 }
2200
2201 SaveUndoState();
2202
2203 // Create a temporary bitmap for the rotated result
2204 gfx::Bitmap rotated_bitmap;
2205 rotated_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
2206
2207 // Rotate 90 degrees clockwise
2208 for (int y = 0; y < 16; ++y) {
2209 for (int x = 0; x < 16; ++x) {
2210 int src_index = y * 16 + x;
2211 int dst_index = x * 16 + (15 - y);
2212 if (src_index < current_tile16_bmp_.size() &&
2213 dst_index < rotated_bitmap.size()) {
2214 rotated_bitmap.WriteToPixel(dst_index,
2215 current_tile16_bmp_.data()[src_index]);
2216 }
2217 }
2218 }
2219
2220 // Copy the rotated result back
2221 current_tile16_bmp_ = std::move(rotated_bitmap);
2223
2224 // Track this tile as having pending changes
2226
2227 return absl::OkStatus();
2228}
2229
2230absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
2231 if (tile8_id < 0 ||
2232 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2233 return absl::InvalidArgumentError("Invalid tile8 ID");
2234 }
2235
2236 if (!current_gfx_individual_[tile8_id].is_active()) {
2237 return absl::FailedPreconditionError("Source tile8 not active");
2238 }
2239
2240 SaveUndoState();
2241
2242 // Fill all four quadrants with the same tile8
2243 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2244 int start_x = (quadrant % 2) * 8;
2245 int start_y = (quadrant / 2) * 8;
2246
2247 for (int y = 0; y < 8; ++y) {
2248 for (int x = 0; x < 8; ++x) {
2249 int src_index = y * 8 + x;
2250 int dst_index = (start_y + y) * 16 + (start_x + x);
2251
2252 if (src_index < current_gfx_individual_[tile8_id].size() &&
2253 dst_index < current_tile16_bmp_.size()) {
2254 uint8_t pixel_value =
2255 current_gfx_individual_[tile8_id].data()[src_index];
2256 current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
2257 }
2258 }
2259 }
2260 }
2261
2263 // Queue texture update via Arena's deferred system
2266
2267 // Track this tile as having pending changes
2269
2270 return absl::OkStatus();
2271}
2272
2275 return absl::FailedPreconditionError("No active tile16 to clear");
2276 }
2277
2278 SaveUndoState();
2279
2280 // Fill with transparent/background color (0)
2281 auto& data = current_tile16_bmp_.mutable_data();
2282 std::fill(data.begin(), data.end(), 0);
2283
2285 // Queue texture update via Arena's deferred system
2288
2289 // Track this tile as having pending changes
2291
2292 return absl::OkStatus();
2293}
2294
2295// Palette management
2296absl::Status Tile16Editor::CyclePalette(bool forward) {
2297 uint8_t new_palette = current_palette_;
2298
2299 if (forward) {
2300 new_palette = (new_palette + 1) % 8;
2301 } else {
2302 new_palette = (new_palette == 0) ? 7 : new_palette - 1;
2303 }
2304
2305 current_palette_ = new_palette;
2306
2307 // Use the RefreshAllPalettes method which handles all the coordination
2309
2310 util::logf("Cycled to palette slot %d", current_palette_);
2311 return absl::OkStatus();
2312}
2313
2314absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
2315 if (!show_palette_preview_) {
2316 return absl::OkStatus();
2317 }
2318
2319 if (palette_id >= 8) {
2320 return absl::InvalidArgumentError("Invalid palette ID");
2321 }
2322
2323 // Create a preview bitmap with the new palette
2324 if (!preview_tile16_.is_active()) {
2326 } else {
2327 // Recreate the preview bitmap with new data
2329 }
2330
2331 if (!game_data())
2332 return absl::FailedPreconditionError("GameData not available");
2333 const auto& ow_main_pal_group = game_data()->palette_groups.overworld_main;
2334 const gfx::SnesPalette* display_palette = nullptr;
2335 if (overworld_palette_.size() >= 256) {
2336 display_palette = &overworld_palette_;
2337 } else if (palette_.size() >= 256) {
2338 display_palette = &palette_;
2339 } else if (ow_main_pal_group.size() > 0) {
2340 display_palette = &ow_main_pal_group[0];
2341 }
2342
2343 if (display_palette && !display_palette->empty()) {
2344 if (auto_normalize_pixels_ && ow_main_pal_group.size() > palette_id) {
2345 preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0],
2346 palette_id);
2347 } else {
2348 preview_tile16_.SetPalette(*display_palette);
2349 }
2350 // Queue texture update via Arena's deferred system
2353 preview_dirty_ = true;
2354 }
2355
2356 return absl::OkStatus();
2357}
2358
2359// Undo/Redo system
2362 return;
2363 }
2364
2365 UndoState state;
2366 state.tile_id = current_tile16_;
2367 state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
2369 state.palette = current_palette_;
2370 state.x_flip = x_flip;
2371 state.y_flip = y_flip;
2372 state.priority = priority_tile;
2373
2374 undo_stack_.push_back(std::move(state));
2375
2376 // Limit undo stack size
2377 if (undo_stack_.size() > kMaxUndoStates_) {
2378 undo_stack_.erase(undo_stack_.begin());
2379 }
2380
2381 // Clear redo stack when new action is performed
2382 redo_stack_.clear();
2383}
2384
2385absl::Status Tile16Editor::Undo() {
2386 if (undo_stack_.empty()) {
2387 return absl::FailedPreconditionError("Nothing to undo");
2388 }
2389
2390 // Save current state to redo stack
2391 UndoState current_state;
2392 current_state.tile_id = current_tile16_;
2393 current_state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
2395 current_state.palette = current_palette_;
2396 current_state.x_flip = x_flip;
2397 current_state.y_flip = y_flip;
2398 current_state.priority = priority_tile;
2399 redo_stack_.push_back(std::move(current_state));
2400
2401 // Restore previous state
2402 const UndoState& previous_state = undo_stack_.back();
2403 current_tile16_ = previous_state.tile_id;
2404 current_tile16_bmp_ = previous_state.tile_bitmap;
2405 current_palette_ = previous_state.palette;
2406 x_flip = previous_state.x_flip;
2407 y_flip = previous_state.y_flip;
2408 priority_tile = previous_state.priority;
2409
2410 // Queue texture update via Arena's deferred system
2413 undo_stack_.pop_back();
2414
2415 return absl::OkStatus();
2416}
2417
2418absl::Status Tile16Editor::Redo() {
2419 if (redo_stack_.empty()) {
2420 return absl::FailedPreconditionError("Nothing to redo");
2421 }
2422
2423 // Save current state to undo stack
2424 SaveUndoState();
2425
2426 // Restore next state
2427 const UndoState& next_state = redo_stack_.back();
2428 current_tile16_ = next_state.tile_id;
2429 current_tile16_bmp_ = next_state.tile_bitmap;
2430 current_palette_ = next_state.palette;
2431 x_flip = next_state.x_flip;
2432 y_flip = next_state.y_flip;
2433 priority_tile = next_state.priority;
2434
2435 // Queue texture update via Arena's deferred system
2438 redo_stack_.pop_back();
2439
2440 return absl::OkStatus();
2441}
2442
2444 if (!tile16_blockset_) {
2445 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2446 }
2447
2448 if (current_tile16_ < 0 ||
2449 current_tile16_ >= static_cast<int>(tile16_blockset_->atlas.size())) {
2450 return absl::OutOfRangeError("Current tile16 ID out of range");
2451 }
2452
2453 if (current_palette_ >= 8) {
2454 return absl::OutOfRangeError("Current palette ID out of range");
2455 }
2456
2457 return absl::OkStatus();
2458}
2459
2460bool Tile16Editor::IsTile16Valid(int tile_id) const {
2461 return tile_id >= 0 && tile16_blockset_ &&
2462 tile_id < static_cast<int>(tile16_blockset_->atlas.size());
2463}
2464
2465// Integration with overworld system
2467 if (!rom_) {
2468 return absl::FailedPreconditionError("ROM not available");
2469 }
2470
2472 return absl::FailedPreconditionError("No active tile16 to save");
2473 }
2474
2475 // Write the tile16 data to ROM first
2477
2478 // Update the tile16 blockset with current changes
2480
2481 // Commit changes to the tile16 blockset
2483
2484 // Mark ROM as dirty so changes persist when saving
2485 rom_->set_dirty(true);
2486
2487 util::logf("Tile16 %d saved to ROM", current_tile16_);
2488 return absl::OkStatus();
2489}
2490
2492 if (!tile16_blockset_) {
2493 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2494 }
2495
2496 if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
2497 return absl::OutOfRangeError("Current tile16 ID out of range");
2498 }
2499
2500 // CRITICAL FIX: Update atlas directly instead of using problematic tile cache
2501 // This prevents the move-related crashes we experienced earlier
2502
2503 // Update the atlas if needed
2505 // Update the portion of the atlas that corresponds to this tile
2506 constexpr int kTilesPerRow =
2507 8; // Standard SNES tile16 layout is 8 tiles per row
2508 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
2509 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
2510
2511 // Copy pixel data from current tile to atlas
2512 for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
2513 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
2514 ++tile_x_offset) {
2515 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
2516 int dst_index =
2517 (tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
2518 (tile_x + tile_x_offset);
2519
2520 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
2521 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
2523 dst_index, current_tile16_bmp_.data()[src_index]);
2524 }
2525 }
2526 }
2527
2529 // Queue texture update via Arena's deferred system
2532 }
2533
2534 return absl::OkStatus();
2535}
2536
2538 if (!tile16_blockset_) {
2539 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2540 }
2541
2542 // Regenerate the tilemap data if needed
2544 // Queue texture update via Arena's deferred system
2547 }
2548
2549 // Update individual cached tiles
2550 // Note: With the new tile cache system, tiles are automatically managed
2551 // and don't need manual modification tracking like the old system
2552 // The cache handles LRU eviction and automatic updates
2553
2554 return absl::OkStatus();
2555}
2556
2558 // CRITICAL FIX: Complete workflow for tile16 changes
2559 // This method now only commits to ROM when explicitly called (user presses
2560 // Save)
2561
2562 // Step 1: Update ROM data with current tile16 changes
2564
2565 // Step 2: Update the local blockset to reflect changes
2567
2568 // Step 3: Update the atlas directly (bypass problematic tile cache)
2570 // Calculate the position of this tile in the blockset atlas
2571 constexpr int kTilesPerRow = 8;
2572 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
2573 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
2574
2575 // Copy current tile16 bitmap data directly to atlas
2576 for (int ty = 0; ty < kTile16Size; ++ty) {
2577 for (int tx = 0; tx < kTile16Size; ++tx) {
2578 int src_index = ty * kTile16Size + tx;
2579 int dst_index =
2580 (tile_y + ty) * tile16_blockset_->atlas.width() + (tile_x + tx);
2581
2582 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
2583 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
2585 dst_index, current_tile16_bmp_.data()[src_index]);
2586 }
2587 }
2588 }
2589
2591 // Queue texture update via Arena's deferred system
2594 }
2595
2596 // Step 4: Notify the parent editor (overworld editor) to regenerate its
2597 // blockset
2600 }
2601
2602 util::logf("Committed Tile16 %d changes to overworld system",
2604 return absl::OkStatus();
2605}
2606
2608 // Reload the current tile16 from ROM to discard any local changes
2610
2611 util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
2612 return absl::OkStatus();
2613}
2614
2616 if (pending_tile16_changes_.empty()) {
2617 return absl::OkStatus(); // Nothing to commit
2618 }
2619
2620 util::logf("Committing %zu pending tile16 changes to ROM",
2622
2623 // Write all pending changes to ROM
2624 for (const auto& [tile_id, tile_data] : pending_tile16_changes_) {
2625 auto status = rom_->WriteTile16(tile_id, zelda3::kTile16Ptr, tile_data);
2626 if (!status.ok()) {
2627 util::logf("Failed to write tile16 %d: %s", tile_id,
2628 status.message().data());
2629 return status;
2630 }
2631 }
2632
2633 // Clear pending changes
2636
2637 // Refresh the blockset to show committed changes
2639
2640 // Notify parent editor to refresh overworld display
2643 }
2644
2645 rom_->set_dirty(true);
2646 util::logf("All pending tile16 changes committed successfully");
2647 return absl::OkStatus();
2648}
2649
2651 if (pending_tile16_changes_.empty()) {
2652 return;
2653 }
2654
2655 util::logf("Discarding %zu pending tile16 changes",
2657
2660
2661 // Reload current tile to restore original state
2663}
2664
2667 if (it != pending_tile16_changes_.end()) {
2668 pending_tile16_changes_.erase(it);
2670 util::logf("Discarded pending changes for tile %d", current_tile16_);
2671 }
2672
2673 // Reload tile from ROM
2675}
2676
2678 // Store the current tile16 data as a pending change
2679 if (auto* tile_data = GetCurrentTile16Data()) {
2681
2682 // Store a copy of the current bitmap for preview
2684
2685 util::logf("Marked tile %d as modified (total pending: %zu)",
2687 }
2688}
2689
2690absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
2691 // Get the current tile16 data from ROM
2692 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 512) {
2693 return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
2694 }
2695
2696 // Determine which quadrant of the tile16 was clicked
2697 int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
2698 int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
2699 int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
2700
2701 // Get the tile16 data structure
2702 auto* tile16_data = GetCurrentTile16Data();
2703 if (!tile16_data) {
2704 return absl::FailedPreconditionError("Failed to get tile16 data");
2705 }
2706
2707 // Extract the tile8 ID from the appropriate quadrant
2708 gfx::TileInfo tile_info;
2709 switch (quadrant) {
2710 case 0:
2711 tile_info = tile16_data->tile0_;
2712 break; // Top-left
2713 case 1:
2714 tile_info = tile16_data->tile1_;
2715 break; // Top-right
2716 case 2:
2717 tile_info = tile16_data->tile2_;
2718 break; // Bottom-left
2719 case 3:
2720 tile_info = tile16_data->tile3_;
2721 break; // Bottom-right
2722 }
2723
2724 // Set the current tile8 and palette
2725 current_tile8_ = tile_info.id_;
2726 current_palette_ = tile_info.palette_;
2727
2728 // Update the flip states based on the tile info
2729 x_flip = tile_info.horizontal_mirror_;
2730 y_flip = tile_info.vertical_mirror_;
2731 priority_tile = tile_info.over_;
2732
2733 // Refresh the palette to match the picked tile
2736
2737 util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
2739
2740 return absl::OkStatus();
2741}
2742
2743// Get the appropriate palette slot for current graphics sheet
2744int Tile16Editor::GetPaletteSlotForSheet(int sheet_index) const {
2745 // Based on ProcessGraphicsBuffer logic and overworld palette coordination:
2746 // Sheets 0,3-6: Use AUX palettes (slots 10-15 in 256-color palette)
2747 // Sheets 1-2: Use MAIN palette (slots 2-6 in 256-color palette)
2748 // Sheet 7: Use ANIMATED palette (slot 7 in 256-color palette)
2749
2750 switch (sheet_index) {
2751 case 0:
2752 return 10; // Main blockset -> AUX1 palette region
2753 case 1:
2754 return 2; // Main graphics -> MAIN palette region
2755 case 2:
2756 return 3; // Main graphics -> MAIN palette region
2757 case 3:
2758 return 11; // Area graphics -> AUX1 palette region
2759 case 4:
2760 return 12; // Area graphics -> AUX1 palette region
2761 case 5:
2762 return 13; // Area graphics -> AUX2 palette region
2763 case 6:
2764 return 14; // Area graphics -> AUX2 palette region
2765 case 7:
2766 return 7; // Animated tiles -> ANIMATED palette region
2767 default:
2768 return static_cast<int>(
2769 current_palette_); // Use current selection for other sheets
2770 }
2771}
2772
2773// NEW: Get the actual palette slot for a given palette button and sheet index
2774// This uses row-based addressing to match the overworld's approach:
2775// The 256-color palette is organized as 16 rows of 16 colors each.
2776// Palette buttons 0-7 map to CGRAM rows starting at the sheet's base row,
2777// skipping HUD rows for overworld visuals.
2779 int sheet_index) const {
2780 const int clamped_button = std::clamp(palette_button, 0, 7);
2781 const int base_row = GetPaletteBaseForSheet(sheet_index);
2782 const int actual_row = std::clamp(base_row + clamped_button, 0, 15);
2783
2784 // Palette buttons map to CGRAM rows starting from the sheet base.
2785 return actual_row * 16;
2786}
2787
2788// NEW: Get the sheet index for a given tile8 ID
2790 // Determine which graphics sheet a tile8 belongs to based on its position
2791 // This is based on the 256-tile per sheet organization
2792
2793 constexpr int kTilesPerSheet = 256; // 16x16 tiles per sheet
2794 int sheet_index = tile8_id / kTilesPerSheet;
2795
2796 // Clamp to valid sheet range (0-7)
2797 return std::min(7, std::max(0, sheet_index));
2798}
2799
2800// NEW: Get the actual palette slot for the current tile16 being edited
2802 // For the current tile16, we need to determine which sheet the tile8s belong
2803 // to and use the most appropriate palette region
2804
2805 if (current_tile8_ >= 0 &&
2806 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2807 int sheet_index = GetSheetIndexForTile8(current_tile8_);
2808 return GetActualPaletteSlot(current_palette_, sheet_index);
2809 }
2810
2811 // Default to sheet 0 (main blockset) if no tile8 selected
2813}
2814
2815int Tile16Editor::GetPaletteBaseForSheet(int sheet_index) const {
2816 // Based on overworld palette structure and how ProcessGraphicsBuffer assigns
2817 // colors: The 256-color palette is organized as 16 rows of 16 colors each.
2818 // Different graphics sheets map to different palette regions:
2819 //
2820 // Row 0: Transparent/system colors
2821 // Row 1: HUD colors (palette index 0x10-0x1F)
2822 // Rows 2-4: MAIN/AUX1 palette region for main graphics
2823 // Rows 5-7: AUX2 palette region for area-specific graphics
2824 // Row 7: ANIMATED palette for animated tiles
2825 //
2826 // The palette_button (0-7) selects within the region.
2827 switch (sheet_index) {
2828 case 0: // Main blockset
2829 case 3: // Area graphics set 1
2830 case 4: // Area graphics set 2
2831 return 2; // AUX1 palette region starts at row 2
2832 case 5: // Area graphics set 3
2833 case 6: // Area graphics set 4
2834 return 5; // AUX2 palette region starts at row 5
2835 case 1: // Main graphics
2836 case 2: // Main graphics
2837 return 2; // MAIN palette region starts at row 2
2838 case 7: // Animated tiles
2839 return 7; // ANIMATED palette region at row 7
2840 default:
2841 return 2; // Default to MAIN region
2842 }
2843}
2844
2846 const gfx::SnesPalette& source, int target_row) const {
2847 // Create a remapped 256-color palette where all pixel values (0-255)
2848 // are mapped to the target palette row based on their low nibble.
2849 //
2850 // This allows the source bitmap (which has pre-encoded palette offsets)
2851 // to be viewed with the user-selected palette row.
2852 //
2853 // For each palette index i:
2854 // - Extract the color index: low_nibble = i & 0x0F
2855 // - Map to target row: (base_row + target_row) * 16 + low_nibble
2856 // - Copy the color from source palette at that position
2857
2858 gfx::SnesPalette remapped;
2859
2860 // Map palette buttons to actual CGRAM rows based on the current sheet.
2861 int sheet_index = 0;
2862 if (current_tile8_ >= 0 &&
2863 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2865 }
2866 const int base_row = GetPaletteBaseForSheet(sheet_index);
2867 const int actual_target_row = std::clamp(base_row + target_row, 0, 15);
2868
2869 for (int i = 0; i < 256; ++i) {
2870 int low_nibble = i & 0x0F;
2871 int target_index = (actual_target_row * 16) + low_nibble;
2872
2873 // Make color 0 of each row transparent
2874 if (low_nibble == 0) {
2875 // Use transparent color (alpha = 0)
2876 remapped.AddColor(gfx::SnesColor(0));
2877 } else if (target_index < static_cast<int>(source.size())) {
2878 remapped.AddColor(source[target_index]);
2879 } else {
2880 // Fallback to black if out of bounds
2881 remapped.AddColor(gfx::SnesColor(0));
2882 }
2883 }
2884
2885 return remapped;
2886}
2887
2888int Tile16Editor::GetEncodedPaletteRow(uint8_t pixel_value) const {
2889 // Determine which palette row a pixel value encodes
2890 // ProcessGraphicsBuffer adds 0x88 (136) to sheets 0, 3, 4, 5
2891 // So pixel values map to rows as follows:
2892 // 0x00-0x0F (0-15): Row 0
2893 // 0x10-0x1F (16-31): Row 1
2894 // ...
2895 // 0x80-0x8F (128-143): Row 8
2896 // 0x90-0x9F (144-159): Row 9
2897 // etc.
2898 return pixel_value / 16;
2899}
2900
2903 return;
2904 }
2905
2906 const gfx::SnesPalette* display_palette = nullptr;
2907 if (overworld_palette_.size() >= 256) {
2908 display_palette = &overworld_palette_;
2909 } else if (palette_.size() >= 256) {
2910 display_palette = &palette_;
2911 } else if (game_data() &&
2912 !game_data()->palette_groups.overworld_main.empty()) {
2913 display_palette =
2915 }
2916
2917 if (!display_palette || display_palette->empty()) {
2918 return;
2919 }
2920
2922 const int palette_slot = GetActualPaletteSlotForCurrentTile16();
2923
2924 // Apply sub-palette with transparent color 0 using computed slot
2925 // SNES palette offset fix: add 1 to skip transparent color slot
2926 // SNES 4BPP uses 16 colors (transparent + 15)
2927 if (palette_slot >= 0 &&
2928 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
2930 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
2931 } else {
2932 current_tile16_bmp_.SetPaletteWithTransparent(*display_palette, 1, 15);
2933 }
2934 } else {
2935 current_tile16_bmp_.SetPalette(*display_palette);
2936 }
2937
2941}
2942
2943// Helper methods for palette management
2944absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
2945 if (tile8_id < 0 ||
2946 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2947 return absl::InvalidArgumentError("Invalid tile8 ID");
2948 }
2949
2950 if (!current_gfx_individual_[tile8_id].is_active()) {
2951 return absl::OkStatus(); // Skip inactive tiles
2952 }
2953
2954 if (!rom_) {
2955 return absl::FailedPreconditionError("ROM not set");
2956 }
2957
2958 // Use the complete 256-color overworld palette for consistency
2959 gfx::SnesPalette display_palette;
2960 if (overworld_palette_.size() >= 256) {
2961 display_palette = overworld_palette_;
2962 } else if (palette_.size() >= 256) {
2963 display_palette = palette_;
2964 } else if (game_data()) {
2965 // Fallback to GameData palette
2966 const auto& palette_groups = game_data()->palette_groups;
2967 if (palette_groups.overworld_main.size() > 0) {
2968 display_palette = palette_groups.overworld_main[0];
2969 } else {
2970 return absl::FailedPreconditionError("No overworld palette available");
2971 }
2972 }
2973
2974 // Validate current_palette_ index
2975 if (current_palette_ < 0 || current_palette_ >= 8) {
2976 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2977 current_palette_ = 0;
2978 }
2979
2980 const int sheet_index = GetSheetIndexForTile8(tile8_id);
2981 const int palette_slot =
2982 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
2983
2984 // Apply palette based on whether pixel values retain CGRAM row offsets.
2985 if (!display_palette.empty()) {
2987 if (palette_slot >= 0 &&
2988 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
2989 current_gfx_individual_[tile8_id].SetPaletteWithTransparent(
2990 display_palette, static_cast<size_t>(palette_slot + 1), 15);
2991 } else {
2992 current_gfx_individual_[tile8_id].SetPaletteWithTransparent(
2993 display_palette, 1, 15);
2994 }
2995 } else {
2996 current_gfx_individual_[tile8_id].SetPalette(display_palette);
2997 }
2998 }
2999
3000 current_gfx_individual_[tile8_id].set_modified(true);
3001 // Queue texture update via Arena's deferred system
3003 &current_gfx_individual_[tile8_id]);
3004
3005 util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
3006 tile8_id, current_palette_, display_palette.size());
3007
3008 return absl::OkStatus();
3009}
3010
3012 if (!rom_) {
3013 return absl::FailedPreconditionError("ROM not set");
3014 }
3015
3016 // Validate current_palette_ index
3017 if (current_palette_ < 0 || current_palette_ >= 8) {
3018 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
3019 current_palette_ = 0;
3020 }
3021
3022 // CRITICAL FIX: Use the complete overworld palette for proper color
3023 // coordination
3024 gfx::SnesPalette display_palette;
3025
3026 if (overworld_palette_.size() >= 256) {
3027 // Use the complete 256-color palette from overworld editor
3028 display_palette = overworld_palette_;
3029 util::logf("Using complete overworld palette with %zu colors",
3030 display_palette.size());
3031 } else if (palette_.size() >= 256) {
3032 // Fallback to the old palette_ if it's complete
3033 display_palette = palette_;
3034 util::logf("Using fallback complete palette with %zu colors",
3035 display_palette.size());
3036 } else if (game_data()) {
3037 // Last resort: Use GameData palette groups
3038 const auto& palette_groups = game_data()->palette_groups;
3039 if (palette_groups.overworld_main.size() > 0) {
3040 display_palette = palette_groups.overworld_main[0];
3041 util::logf("Warning: Using ROM main palette with %zu colors",
3042 display_palette.size());
3043 } else {
3044 return absl::FailedPreconditionError("No palette available");
3045 }
3046 }
3047
3048 if (display_palette.empty()) {
3049 return absl::FailedPreconditionError("Display palette empty");
3050 }
3051
3052 // The source bitmap (current_gfx_bmp_) contains 8bpp indexed pixel data.
3053 // If palette offsets are preserved, apply the full CGRAM palette. Otherwise,
3054 // remap the palette to the user-selected row.
3057 gfx::SnesPalette remapped_palette =
3059 current_gfx_bmp_.SetPalette(remapped_palette);
3060 util::logf("Applied remapped palette (button %d) to source bitmap",
3062 } else {
3063 current_gfx_bmp_.SetPalette(display_palette);
3064 util::logf("Applied full CGRAM palette to source bitmap");
3065 }
3066
3068 // Queue texture update via Arena's deferred system
3071 }
3072
3073 // Update current tile16 being edited
3076 // Use sheet-aware palette slot for current tile16
3077 // SNES palette offset fix: add 1 to skip transparent color slot
3078 int palette_slot = GetActualPaletteSlotForCurrentTile16();
3079
3080 if (palette_slot >= 0 &&
3081 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
3083 display_palette, static_cast<size_t>(palette_slot + 1), 15);
3084 } else {
3085 current_tile16_bmp_.SetPaletteWithTransparent(display_palette, 1, 15);
3086 }
3087 } else {
3088 current_tile16_bmp_.SetPalette(display_palette);
3089 }
3090
3094 }
3095
3096 // Update individual tile8 graphics
3097 for (size_t i = 0; i < current_gfx_individual_.size(); ++i) {
3098 if (!current_gfx_individual_[i].is_active()) {
3099 continue;
3100 }
3101
3103 // Calculate per-tile8 palette slot based on which sheet it belongs to
3104 int sheet_index = GetSheetIndexForTile8(static_cast<int>(i));
3105 int palette_slot = GetActualPaletteSlot(current_palette_, sheet_index);
3106
3107 // Apply sub-palette with transparent color 0
3108 if (palette_slot >= 0 &&
3109 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
3110 current_gfx_individual_[i].SetPaletteWithTransparent(
3111 display_palette, static_cast<size_t>(palette_slot + 1), 15);
3112 } else {
3113 // Fallback to slot 1 if computed slot exceeds palette bounds
3114 current_gfx_individual_[i].SetPaletteWithTransparent(display_palette, 1,
3115 15);
3116 }
3117 } else {
3118 current_gfx_individual_[i].SetPalette(display_palette);
3119 }
3120
3121 current_gfx_individual_[i].set_modified(true);
3122 // Queue texture update via Arena's deferred system
3125 }
3126
3127 util::logf(
3128 "Successfully refreshed all palettes in tile16 editor with palette %d",
3130 return absl::OkStatus();
3131}
3132
3134 util::logf("=== TILE8 SOURCE DATA ANALYSIS ===");
3135
3136 // Analyze current_gfx_bmp_
3137 util::logf("current_gfx_bmp_:");
3138 util::logf(" - Active: %s", current_gfx_bmp_.is_active() ? "yes" : "no");
3139 util::logf(" - Size: %dx%d", current_gfx_bmp_.width(),
3141 util::logf(" - Depth: %d bpp", current_gfx_bmp_.depth());
3142 util::logf(" - Data size: %zu bytes", current_gfx_bmp_.size());
3143 util::logf(" - Palette size: %zu colors", current_gfx_bmp_.palette().size());
3144
3145 // Analyze pixel value distribution in first 64 pixels (first tile8)
3146 if (current_gfx_bmp_.data() && current_gfx_bmp_.size() >= 64) {
3147 std::map<uint8_t, int> pixel_counts;
3148 for (size_t i = 0; i < 64; ++i) {
3149 uint8_t val = current_gfx_bmp_.data()[i];
3150 pixel_counts[val]++;
3151 }
3152 util::logf(" - First tile8 (Sheet 0) pixel distribution:");
3153 for (const auto& [val, count] : pixel_counts) {
3154 int row = GetEncodedPaletteRow(val);
3155 int col = val & 0x0F;
3156 util::logf(" Value 0x%02X (%3d) = Row %d, Col %d: %d pixels", val, val,
3157 row, col, count);
3158 }
3159
3160 // Check if values are in expected 4bpp range
3161 bool all_4bpp = true;
3162 for (const auto& [val, count] : pixel_counts) {
3163 if (val > 15) {
3164 all_4bpp = false;
3165 break;
3166 }
3167 }
3168 util::logf(" - Values in raw 4bpp range (0-15): %s",
3169 all_4bpp ? "yes" : "NO (pre-encoded)");
3170
3171 // Show what the remapping does
3172 util::logf(" - Palette remapping for viewing:");
3173 util::logf(" Selected palette: %d (row %d)", current_palette_,
3175 util::logf(" Pixels are remapped: (value & 0x0F) + (selected_row * 16)");
3176 }
3177
3178 // Analyze current_gfx_individual_
3179 util::logf("current_gfx_individual_:");
3180 util::logf(" - Count: %zu tiles", current_gfx_individual_.size());
3181
3182 if (!current_gfx_individual_.empty() &&
3183 current_gfx_individual_[0].is_active()) {
3184 const auto& first_tile = current_gfx_individual_[0];
3185 util::logf(" - First tile:");
3186 util::logf(" - Size: %dx%d", first_tile.width(), first_tile.height());
3187 util::logf(" - Depth: %d bpp", first_tile.depth());
3188 util::logf(" - Palette size: %zu colors", first_tile.palette().size());
3189
3190 if (first_tile.data() && first_tile.size() >= 64) {
3191 std::map<uint8_t, int> pixel_counts;
3192 for (size_t i = 0; i < 64; ++i) {
3193 uint8_t val = first_tile.data()[i];
3194 pixel_counts[val]++;
3195 }
3196 util::logf(" - Pixel distribution:");
3197 for (const auto& [val, count] : pixel_counts) {
3198 util::logf(" Value 0x%02X (%3d): %d pixels", val, val, count);
3199 }
3200 }
3201 }
3202
3203 // Analyze palette state
3204 util::logf("Palette state:");
3205 util::logf(" - current_palette_: %d", current_palette_);
3206 util::logf(" - overworld_palette_ size: %zu", overworld_palette_.size());
3207 util::logf(" - palette_ size: %zu", palette_.size());
3208
3209 // Calculate expected palette slot
3210 int palette_slot = GetActualPaletteSlot(current_palette_, 0);
3211 util::logf(" - GetActualPaletteSlot(%d, 0) = %d", current_palette_,
3212 palette_slot);
3213 util::logf(" - Expected palette offset for SetPaletteWithTransparent: %d",
3214 palette_slot + 1);
3215
3216 // Show first 16 colors of the overworld palette
3217 if (overworld_palette_.size() >= 16) {
3218 util::logf(" - First 16 palette colors (row 0):");
3219 for (int i = 0; i < 16; ++i) {
3220 auto color = overworld_palette_[i];
3221 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3222 static_cast<int>(color.rgb().x),
3223 static_cast<int>(color.rgb().y),
3224 static_cast<int>(color.rgb().z));
3225 }
3226 }
3227
3228 // Show colors at the selected palette slot
3229 if (overworld_palette_.size() >= static_cast<size_t>(palette_slot + 16)) {
3230 util::logf(" - Colors at palette slot %d (row %d):", palette_slot,
3231 palette_slot / 16);
3232 for (int i = 0; i < 16; ++i) {
3233 auto color = overworld_palette_[palette_slot + i];
3234 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3235 static_cast<int>(color.rgb().x),
3236 static_cast<int>(color.rgb().y),
3237 static_cast<int>(color.rgb().z));
3238 }
3239 }
3240
3241 util::logf("=== END ANALYSIS ===");
3242}
3243
3246 if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
3247 Text("Pixel Normalization & Color Correction:");
3248
3249 int mask_value = static_cast<int>(palette_normalization_mask_);
3250 if (SliderInt("Normalization Mask", &mask_value, 1, 255, "0x%02X")) {
3251 palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
3252 }
3253
3254 Checkbox("Auto Normalize Pixels", &auto_normalize_pixels_);
3255
3256 if (Button("Apply to All Graphics")) {
3257 auto reload_result = LoadTile8();
3258 if (!reload_result.ok()) {
3259 Text("Error: %s", reload_result.message().data());
3260 }
3261 }
3262
3263 SameLine();
3264 if (Button("Reset Defaults")) {
3267 auto reload_result = LoadTile8();
3268 (void)reload_result; // Suppress warning
3269 }
3270
3271 Separator();
3272 Text("Current State:");
3273 static constexpr std::array<const char*, 7> palette_group_names = {
3274 "OW Main", "OW Aux", "OW Anim", "Dungeon",
3275 "Sprites", "Armor", "Sword"};
3276 Text("Palette Group: %d (%s)", current_palette_group_,
3278 ? palette_group_names[current_palette_group_]
3279 : "Unknown");
3280 Text("Current Palette: %d", current_palette_);
3281
3282 Separator();
3283 Text("Sheet-Specific Fixes:");
3284
3285 // Sheet-specific palette fixes
3286 static bool fix_sheet_0 = true;
3287 static bool fix_sprite_sheets = true;
3288 static bool use_transparent_for_terrain = false;
3289
3290 if (Checkbox("Fix Sheet 0 (Trees)", &fix_sheet_0)) {
3291 auto reload_result = LoadTile8();
3292 if (!reload_result.ok()) {
3293 Text("Error reloading: %s", reload_result.message().data());
3294 }
3295 }
3296 HOVER_HINT(
3297 "Use direct palette for sheet 0 instead of transparent palette");
3298
3299 if (Checkbox("Fix Sprite Sheets", &fix_sprite_sheets)) {
3300 auto reload_result = LoadTile8();
3301 if (!reload_result.ok()) {
3302 Text("Error reloading: %s", reload_result.message().data());
3303 }
3304 }
3305 HOVER_HINT("Use direct palette for sprite graphics sheets");
3306
3307 if (Checkbox("Transparent for Terrain", &use_transparent_for_terrain)) {
3308 auto reload_result = LoadTile8();
3309 if (!reload_result.ok()) {
3310 Text("Error reloading: %s", reload_result.message().data());
3311 }
3312 }
3313 HOVER_HINT("Force transparent palette for terrain graphics");
3314
3315 Separator();
3316 Text("Color Analysis:");
3317 if (current_tile8_ >= 0 &&
3318 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
3320 Text("Selected Tile8 Analysis:");
3321 const auto& tile_data =
3323 std::map<uint8_t, int> pixel_counts;
3324 for (uint8_t pixel : tile_data) {
3325 pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
3326 }
3327
3328 Text("Pixel Value Distribution:");
3329 for (const auto& pair : pixel_counts) {
3330 int value = pair.first;
3331 int count = pair.second;
3332 Text(" Value %d (0x%X): %d pixels", value, value, count);
3333 }
3334
3335 Text("Palette Colors Used:");
3336 const auto& palette = current_gfx_individual_[current_tile8_].palette();
3337 for (const auto& pair : pixel_counts) {
3338 int value = pair.first;
3339 int count = pair.second;
3340 if (value < static_cast<int>(palette.size())) {
3341 auto color = palette[value];
3342 ImVec4 display_color = color.rgb();
3343 ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
3344 display_color, ImGuiColorEditFlags_NoTooltip,
3345 ImVec2(16, 16));
3346 if (ImGui::IsItemHovered()) {
3347 ImGui::SetTooltip("Index %d: 0x%04X (%d pixels)", value,
3348 color.snes(), count);
3349 }
3350 if (value % 8 != 7)
3351 ImGui::SameLine();
3352 }
3353 }
3354 }
3355
3356 // Enhanced ROM Palette Management Section
3357 Separator();
3358 if (CollapsingHeader("ROM Palette Manager") && rom_) {
3359 Text("Experimental ROM Palette Selection:");
3360 HOVER_HINT(
3361 "Use ROM palettes to experiment with different color schemes");
3362
3363 if (Button("Open Enhanced Palette Editor")) {
3365 }
3366 SameLine();
3367 if (Button("Show Color Analysis")) {
3369 }
3370
3371 // Quick palette application
3372 static int quick_group = 0;
3373 static int quick_index = 0;
3374
3375 SliderInt("ROM Group", &quick_group, 0, 6);
3376 SliderInt("Palette Index", &quick_index, 0, 7);
3377
3378 if (Button("Apply to Tile8 Source")) {
3379 if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3380 util::logf("Applied ROM palette group %d, index %d to Tile8 source",
3381 quick_group, quick_index);
3382 }
3383 }
3384 SameLine();
3385 if (Button("Apply to Tile16 Editor")) {
3386 if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3387 util::logf(
3388 "Applied ROM palette group %d, index %d to Tile16 editor",
3389 quick_group, quick_index);
3390 }
3391 }
3392 }
3393 }
3394 End();
3395 }
3396}
3397
3399 Text("Layout Scratch:");
3400 for (int i = 0; i < 4; i++) {
3401 if (i > 0)
3402 SameLine();
3403 std::string slot_name = "S" + std::to_string(i + 1);
3404
3405 if (layout_scratch_[i].in_use) {
3406 if (Button((slot_name + " Load").c_str(), ImVec2(40, 20))) {
3407 // Load layout from scratch - placeholder for now
3408 }
3409 } else {
3410 if (Button((slot_name + " Save").c_str(), ImVec2(40, 20))) {
3411 // Save current layout to scratch - placeholder for now
3412 }
3413 }
3414 }
3415}
3416
3418 if (slot < 0 || slot >= 4) {
3419 return absl::InvalidArgumentError("Invalid scratch slot");
3420 }
3421
3422 // For now, just mark as used - full implementation would save current editing
3423 // state
3424 layout_scratch_[slot].in_use = true;
3425 layout_scratch_[slot].name = absl::StrFormat("Layout %d", slot + 1);
3426
3427 return absl::OkStatus();
3428}
3429
3431 if (slot < 0 || slot >= 4) {
3432 return absl::InvalidArgumentError("Invalid scratch slot");
3433 }
3434
3435 if (!layout_scratch_[slot].in_use) {
3436 return absl::FailedPreconditionError("Scratch slot is empty");
3437 }
3438
3439 // Placeholder - full implementation would restore editing state
3440 return absl::OkStatus();
3441}
3442
3444 if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
3445 ImGuiWindowFlags_AlwaysAutoResize)) {
3446 ImGui::Text("Manual Tile8 Configuration for Tile16 %02X", current_tile16_);
3447 ImGui::Separator();
3448
3449 auto* tile_data = GetCurrentTile16Data();
3450 if (tile_data) {
3451 ImGui::Text("Current Tile16 ROM Data:");
3452
3453 // Display and edit each quadrant using TileInfo structure
3454 const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
3455 "Bottom-Right"};
3456
3457 for (int q = 0; q < 4; q++) {
3458 ImGui::Text("%s Quadrant:", quadrant_names[q]);
3459
3460 // Get the current TileInfo for this quadrant
3461 gfx::TileInfo* tile_info = nullptr;
3462 switch (q) {
3463 case 0:
3464 tile_info = &tile_data->tile0_;
3465 break;
3466 case 1:
3467 tile_info = &tile_data->tile1_;
3468 break;
3469 case 2:
3470 tile_info = &tile_data->tile2_;
3471 break;
3472 case 3:
3473 tile_info = &tile_data->tile3_;
3474 break;
3475 }
3476
3477 if (tile_info) {
3478 // Editable inputs for TileInfo components
3479 ImGui::PushID(q);
3480
3481 int tile_id_int = static_cast<int>(tile_info->id_);
3482 if (ImGui::InputInt("Tile8 ID", &tile_id_int, 1, 10)) {
3483 tile_info->id_ =
3484 static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
3485 }
3486
3487 int palette_int = static_cast<int>(tile_info->palette_);
3488 if (ImGui::SliderInt("Palette", &palette_int, 0, 7)) {
3489 tile_info->palette_ = static_cast<uint8_t>(palette_int);
3490 }
3491
3492 ImGui::Checkbox("X Flip", &tile_info->horizontal_mirror_);
3493 ImGui::SameLine();
3494 ImGui::Checkbox("Y Flip", &tile_info->vertical_mirror_);
3495 ImGui::SameLine();
3496 ImGui::Checkbox("Priority", &tile_info->over_);
3497
3498 if (ImGui::Button("Apply to Graphics")) {
3499 // Update the tiles_info array and regenerate graphics
3500 tile_data->tiles_info[q] = *tile_info;
3501
3502 auto update_result = UpdateROMTile16Data();
3503 if (!update_result.ok()) {
3504 ImGui::Text("Error: %s", update_result.message().data());
3505 }
3506
3507 auto refresh_result = SetCurrentTile(current_tile16_);
3508 if (!refresh_result.ok()) {
3509 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
3510 }
3511 }
3512
3513 ImGui::PopID();
3514 }
3515
3516 if (q < 3)
3517 ImGui::Separator();
3518 }
3519
3520 ImGui::Separator();
3521 if (ImGui::Button("Apply All Changes")) {
3522 auto update_result = UpdateROMTile16Data();
3523 if (!update_result.ok()) {
3524 ImGui::Text("Update Error: %s", update_result.message().data());
3525 }
3526
3527 auto save_result = SaveTile16ToROM();
3528 if (!save_result.ok()) {
3529 ImGui::Text("Save Error: %s", save_result.message().data());
3530 }
3531 }
3532 ImGui::SameLine();
3533 if (ImGui::Button("Refresh Display")) {
3534 auto refresh_result = SetCurrentTile(current_tile16_);
3535 if (!refresh_result.ok()) {
3536 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
3537 }
3538 }
3539
3540 } else {
3541 ImGui::Text("Tile16 data not accessible");
3542 ImGui::Text("Current tile16: %d", current_tile16_);
3543 if (rom_) {
3544 ImGui::Text("Valid range: 0-4095 (4096 total tiles)");
3545 }
3546 }
3547
3548 ImGui::Separator();
3549 if (ImGui::Button("Close")) {
3550 ImGui::CloseCurrentPopup();
3551 }
3552
3553 ImGui::EndPopup();
3554 }
3555}
3556
3558 // Skip if live preview is disabled
3559 if (!live_preview_enabled_) {
3560 return absl::OkStatus();
3561 }
3562
3563 // Check if preview needs updating
3564 if (!preview_dirty_) {
3565 return absl::OkStatus();
3566 }
3567
3568 // Ensure we have valid tile data
3570 preview_dirty_ = false;
3571 return absl::OkStatus();
3572 }
3573
3574 // Update the preview bitmap from current tile16
3575 if (!preview_tile16_.is_active()) {
3577 } else {
3578 // Recreate with updated data
3580 }
3581
3582 // Apply the current palette
3583 if (game_data()) {
3584 const auto& ow_main_pal_group = game_data()->palette_groups.overworld_main;
3585 const gfx::SnesPalette* display_palette = nullptr;
3586 if (overworld_palette_.size() >= 256) {
3587 display_palette = &overworld_palette_;
3588 } else if (palette_.size() >= 256) {
3589 display_palette = &palette_;
3590 } else if (ow_main_pal_group.size() > 0) {
3591 display_palette = &ow_main_pal_group[0];
3592 }
3593
3594 if (display_palette && !display_palette->empty()) {
3596 ow_main_pal_group.size() > current_palette_) {
3597 preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0],
3599 } else {
3600 preview_tile16_.SetPalette(*display_palette);
3601 }
3602 }
3603 }
3604
3605 // Queue texture update
3608
3609 // Clear the dirty flag
3610 preview_dirty_ = false;
3611
3612 return absl::OkStatus();
3613}
3614
3615} // namespace editor
3616} // namespace yaze
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:258
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:277
void set_dirty(bool dirty)
Definition rom.h:130
absl::Status SaveTile16ToScratchSpace(int slot)
std::map< int, gfx::Tile16 > pending_tile16_changes_
absl::Status LoadLayoutFromScratch(int slot)
zelda3::GameData * game_data() const
std::chrono::steady_clock::time_point last_edit_time_
void DrawContextMenu()
Draw context menu with editor actions.
absl::Status CyclePalette(bool forward=true)
absl::Status FillTile16WithTile8(int tile8_id)
gfx::Tilemap * tile16_blockset_
absl::Status CommitAllChanges()
Write all pending changes to ROM and notify parent.
std::map< int, gfx::Bitmap > pending_tile16_bitmaps_
absl::Status SaveTile16ToROM()
Write current tile16 data directly to ROM (bypasses pending system)
void DiscardAllChanges()
Discard all pending changes (revert to ROM state)
int GetActualPaletteSlotForCurrentTile16() const
Get the palette slot for the current tile being edited.
std::array< uint8_t, 0x200 > all_tiles_types_
absl::Status RegenerateTile16BitmapFromROM()
absl::Status DiscardChanges()
Discard current tile's changes (single tile)
absl::Status PasteTile16FromClipboard()
gui::TileSelectorWidget blockset_selector_
int pending_changes_count() const
Get count of tiles with pending changes.
absl::Status SaveLayoutToScratch(int slot)
absl::Status LoadTile16FromScratchSpace(int slot)
int GetPaletteSlotForSheet(int sheet_index) const
Get base palette slot for a graphics sheet.
gfx::SnesPalette CreateRemappedPaletteForViewing(const gfx::SnesPalette &source, int target_row) const
Create a remapped palette for viewing with user-selected palette.
int GetActualPaletteSlot(int palette_button, int sheet_index) const
Calculate actual palette slot from button + sheet.
std::vector< UndoState > undo_stack_
absl::Status FlipTile16Horizontal()
gfx::SnesPalette overworld_palette_
absl::Status Initialize(const gfx::Bitmap &tile16_blockset_bmp, const gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
static constexpr int kTilesPerPage
int GetEncodedPaletteRow(uint8_t pixel_value) const
Get the encoded palette row for a pixel value.
static constexpr int kTilesPerRow
void EnableLivePreview(bool enable)
absl::Status SetCurrentTile(int id)
absl::Status UpdateTile8Palette(int tile8_id)
Update palette for a specific tile8.
absl::Status UpdateAsPanel()
Update the editor content without MenuBar (for EditorPanel usage)
absl::Status RefreshTile16Blockset()
std::array< gfx::Bitmap, 4 > scratch_space_
absl::Status RefreshAllPalettes()
Refresh all tile8 palettes after a palette change.
void DrawPaletteSettings()
Draw palette settings UI.
std::vector< UndoState > redo_stack_
absl::Status UpdateOverworldTilemap()
Update the overworld tilemap to reflect tile changes.
absl::Status CommitChangesToBlockset()
Commit pending changes to the blockset atlas.
std::array< LayoutScratch, 4 > layout_scratch_
absl::Status UpdateROMTile16Data()
bool IsTile16Valid(int tile_id) const
gfx::Tile16 * GetCurrentTile16Data()
void MarkCurrentTileModified()
Mark the current tile as having pending changes.
std::vector< gfx::Bitmap > current_gfx_individual_
bool has_pending_changes() const
Check if any tiles have uncommitted changes.
absl::Status CommitChangesToOverworld()
Full commit workflow: ROM + blockset + notify parent.
absl::Status UpdateBlocksetBitmap()
void DiscardCurrentTileChanges()
Discard only the current tile's pending changes.
void RequestTileSwitch(int target_tile_id)
absl::Status DrawToCurrentTile16(ImVec2 pos, const gfx::Bitmap *source_tile=nullptr)
absl::Status PreviewPaletteChange(uint8_t palette_id)
absl::Status CopyTile16ToClipboard(int tile_id)
absl::Status ClearScratchSpace(int slot)
bool is_tile_modified(int tile_id) const
Check if a specific tile has pending changes.
std::function< absl::Status()> on_changes_committed_
static constexpr size_t kMaxUndoStates_
int GetPaletteBaseForSheet(int sheet_index) const
Get palette base row for a graphics sheet.
int GetSheetIndexForTile8(int tile8_id) const
Determine which graphics sheet contains a tile8.
std::array< bool, 4 > scratch_space_used_
absl::Status PickTile8FromTile16(const ImVec2 &position)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:35
static Arena & Get()
Definition arena.cc:20
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const uint8_t * data() const
Definition bitmap.h:377
const SnesPalette & palette() const
Definition bitmap.h:368
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:581
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
TextureHandle texture() const
Definition bitmap.h:380
const std::vector< uint8_t > & vector() const
Definition bitmap.h:381
auto size() const
Definition bitmap.h:376
void UpdateSurfacePixels()
Update SDL surface with current pixel data from data_ vector Call this after modifying pixel data via...
Definition bitmap.cc:369
bool is_active() const
Definition bitmap.h:384
void set_modified(bool modified)
Definition bitmap.h:388
int height() const
Definition bitmap.h:374
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:853
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
int width() const
Definition bitmap.h:373
int depth() const
Definition bitmap.h:375
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:456
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:378
SDL_Surface * surface() const
Definition bitmap.h:379
bool modified() const
Definition bitmap.h:383
RAII timer for automatic timing management.
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1075
void ShowScalingControls()
Definition canvas.cc:1852
void ShowAdvancedCanvasProperties()
Definition canvas.cc:1728
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:852
auto custom_labels_enabled()
Definition canvas.h:495
void SetCanvasSize(ImVec2 canvas_size)
Definition canvas.h:469
auto mutable_labels(int i)
Definition canvas.h:539
void InitializePaletteEditor(Rom *rom)
Definition canvas.cc:283
void set_draggable(bool draggable)
Definition canvas.h:457
float GetGlobalScale() const
Definition canvas.h:473
auto zero_point() const
Definition canvas.h:443
bool IsMouseHovering() const
Definition canvas.h:433
void ShowPaletteEditor()
Definition canvas.cc:299
void InitializeDefaults()
Definition canvas.cc:159
void SetAutoResize(bool auto_resize)
Definition canvas.h:370
bool ApplyROMPalette(int group_index, int palette_index)
Definition canvas.cc:321
void ShowColorAnalysis()
Definition canvas.cc:311
RenderResult Render(gfx::Bitmap &atlas, bool atlas_ready)
#define ICON_MD_MENU
Definition icons.h:1196
#define HOVER_HINT(string)
Definition macro.h:24
Definition input.cc:22
constexpr int kTile16PixelCount
constexpr int kTile8PixelCount
constexpr float kTile8DisplayScale
constexpr int kTile8Size
constexpr int kTile16Size
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:270
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1509
void BeginPadding(int i)
Definition style.cc:274
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1486
void AddTableColumn(Table &table, const std::string &label, GuiElement element)
Definition input.cc:640
void EndPadding()
Definition style.cc:278
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:290
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
constexpr int kNumTile16Individual
Definition overworld.h:195
constexpr uint32_t kTile16Ptr
Definition game_data.h:57
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
const SnesPalette & palette_ref(int i) const
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
std::optional< float > grid_step
Definition canvas.h:70
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89