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