Files
yaze/src/app/editor/overworld/tile16_editor.cc

2597 lines
90 KiB
C++

#include "tile16_editor.h"
#include <array>
#include "absl/status/status.h"
#include "app/gfx/backend/irenderer.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/debug/performance/performance_profiler.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gui/canvas/canvas.h"
#include "app/gui/core/input.h"
#include "app/gui/core/style.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#include "util/hex.h"
#include "util/log.h"
#include "util/macro.h"
#include "zelda3/overworld/overworld.h"
namespace yaze {
namespace editor {
using namespace ImGui;
absl::Status Tile16Editor::Initialize(
const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp,
std::array<uint8_t, 0x200>& all_tiles_types) {
all_tiles_types_ = all_tiles_types;
// Copy the graphics bitmap (palette will be set later by overworld editor)
current_gfx_bmp_.Create(current_gfx_bmp.width(), current_gfx_bmp.height(),
current_gfx_bmp.depth(), current_gfx_bmp.vector());
current_gfx_bmp_.SetPalette(current_gfx_bmp.palette()); // Temporary palette
// TODO: Queue texture for later rendering.
// core::Renderer::Get().RenderBitmap(&current_gfx_bmp_);
// Copy the tile16 blockset bitmap
tile16_blockset_bmp_.Create(
tile16_blockset_bmp.width(), tile16_blockset_bmp.height(),
tile16_blockset_bmp.depth(), tile16_blockset_bmp.vector());
tile16_blockset_bmp_.SetPalette(tile16_blockset_bmp.palette());
// TODO: Queue texture for later rendering.
// core::Renderer::Get().RenderBitmap(&tile16_blockset_bmp_);
// Note: LoadTile8() will be called after palette is set by overworld editor
// This ensures proper palette coordination from the start
// Initialize current tile16 bitmap - this will be set by SetCurrentTile
current_tile16_bmp_.Create(kTile16Size, kTile16Size, 8,
std::vector<uint8_t>(kTile16PixelCount, 0));
current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
// TODO: Queue texture for later rendering.
// core::Renderer::Get().RenderBitmap(&current_tile16_bmp_);
// Initialize enhanced canvas features with proper sizing
tile16_edit_canvas_.InitializeDefaults();
tile8_source_canvas_.InitializeDefaults();
// Configure canvases with proper initialization
tile16_edit_canvas_.SetAutoResize(false);
tile8_source_canvas_.SetAutoResize(false);
// Initialize enhanced palette editors if ROM is available
if (rom_) {
tile16_edit_canvas_.InitializePaletteEditor(rom_);
tile8_source_canvas_.InitializePaletteEditor(rom_);
}
// Initialize the current tile16 properly from the blockset
if (tile16_blockset_) {
RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
}
map_blockset_loaded_ = true;
// Setup collision type labels for tile8 canvas
ImVector<std::string> tile16_names;
for (int i = 0; i < 0x200; ++i) {
std::string str = util::HexByte(all_tiles_types_[i]);
tile16_names.push_back(str);
}
*tile8_source_canvas_.mutable_labels(0) = tile16_names;
*tile8_source_canvas_.custom_labels_enabled() = true;
// Setup tile info table
gui::AddTableColumn(tile_edit_table_, "##tile16ID",
[&]() { Text("Tile16: %02X", current_tile16_); });
gui::AddTableColumn(tile_edit_table_, "##tile8ID",
[&]() { Text("Tile8: %02X", current_tile8_); });
gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
Checkbox("X Flip", &x_flip);
Checkbox("Y Flip", &y_flip);
Checkbox("Priority", &priority_tile);
});
return absl::OkStatus();
}
absl::Status Tile16Editor::Update() {
if (!map_blockset_loaded_) {
return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
}
if (BeginMenuBar()) {
if (BeginMenu("View")) {
Checkbox("Show Collision Types",
tile8_source_canvas_.custom_labels_enabled());
EndMenu();
}
if (BeginMenu("Edit")) {
if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
RETURN_IF_ERROR(CopyTile16ToClipboard(current_tile16_));
}
if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
RETURN_IF_ERROR(PasteTile16FromClipboard());
}
EndMenu();
}
if (BeginMenu("File")) {
if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
status_ = SaveTile16ToROM();
}
if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
status_ = CommitChangesToBlockset();
}
Separator();
bool live_preview = live_preview_enabled_;
if (MenuItem("Live Preview", nullptr, &live_preview)) {
EnableLivePreview(live_preview);
}
EndMenu();
}
if (BeginMenu("Scratch Space")) {
for (int i = 0; i < 4; i++) {
std::string slot_name = "Slot " + std::to_string(i + 1);
if (scratch_space_used_[i]) {
if (MenuItem((slot_name + " (Load)").c_str())) {
RETURN_IF_ERROR(LoadTile16FromScratchSpace(i));
}
if (MenuItem((slot_name + " (Save)").c_str())) {
RETURN_IF_ERROR(SaveTile16ToScratchSpace(i));
}
if (MenuItem((slot_name + " (Clear)").c_str())) {
RETURN_IF_ERROR(ClearScratchSpace(i));
}
} else {
if (MenuItem((slot_name + " (Save)").c_str())) {
RETURN_IF_ERROR(SaveTile16ToScratchSpace(i));
}
}
if (i < 3)
Separator();
}
EndMenu();
}
EndMenuBar();
}
// About popup
if (BeginPopupModal("About Tile16 Editor", NULL,
ImGuiWindowFlags_AlwaysAutoResize)) {
Text("Tile16 Editor for Link to the Past");
Text("This editor allows you to edit 16x16 tiles used in the game.");
Text("Features:");
BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
BulletText("Copy and paste Tile16 graphics");
BulletText("Save and load Tile16 graphics to/from scratch space");
BulletText("Preview Tile16 graphics at a larger size");
Separator();
if (Button("Close")) {
CloseCurrentPopup();
}
EndPopup();
}
// Handle keyboard shortcuts
if (!ImGui::IsAnyItemActive()) {
// Editing shortcuts
if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
status_ = ClearTile16();
}
if (ImGui::IsKeyPressed(ImGuiKey_H)) {
status_ = FlipTile16Horizontal();
}
if (ImGui::IsKeyPressed(ImGuiKey_V)) {
status_ = FlipTile16Vertical();
}
if (ImGui::IsKeyPressed(ImGuiKey_R)) {
status_ = RotateTile16();
}
if (ImGui::IsKeyPressed(ImGuiKey_F)) {
if (current_tile8_ >= 0 &&
current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
status_ = FillTile16WithTile8(current_tile8_);
}
}
// Palette shortcuts
if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
status_ = CyclePalette(false);
}
if (ImGui::IsKeyPressed(ImGuiKey_E)) {
status_ = CyclePalette(true);
}
// Palette number shortcuts (1-8)
for (int i = 0; i < 8; ++i) {
if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
current_palette_ = i;
status_ = CyclePalette(true);
status_ = CyclePalette(false);
current_palette_ = i;
}
}
// Undo/Redo with Ctrl
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
status_ = Undo();
}
if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
status_ = Redo();
}
if (ImGui::IsKeyPressed(ImGuiKey_C)) {
status_ = CopyTile16ToClipboard(current_tile16_);
}
if (ImGui::IsKeyPressed(ImGuiKey_V)) {
status_ = PasteTile16FromClipboard();
}
if (ImGui::IsKeyPressed(ImGuiKey_S)) {
if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
ImGui::IsKeyDown(ImGuiKey_RightShift)) {
status_ = CommitChangesToBlockset();
} else {
status_ = SaveTile16ToROM();
}
}
}
}
DrawTile16Editor();
// Draw palette settings popup if enabled
DrawPaletteSettings();
return absl::OkStatus();
}
void Tile16Editor::DrawTile16Editor() {
// REFACTORED: Single unified table layout in UpdateTile16Edit
status_ = UpdateTile16Edit();
}
absl::Status Tile16Editor::UpdateBlockset() {
gui::BeginPadding(2);
gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
blockset_canvas_.DrawBackground();
gui::EndPadding();
blockset_canvas_.DrawContextMenu();
// CRITICAL FIX: Handle single clicks properly like the overworld editor
bool tile_selected = false;
// First, call DrawTileSelector for visual feedback
blockset_canvas_.DrawTileSelector(32.0f);
// Then check for single click to update tile selection
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
blockset_canvas_.IsMouseHovering()) {
tile_selected = true;
}
if (tile_selected) {
// Get mouse position relative to canvas
const ImGuiIO& io = ImGui::GetIO();
ImVec2 canvas_pos = blockset_canvas_.zero_point();
ImVec2 mouse_pos =
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
// Calculate grid position (32x32 tiles in blockset)
int grid_x = static_cast<int>(mouse_pos.x / 32);
int grid_y = static_cast<int>(mouse_pos.y / 32);
int selected_tile = grid_x + grid_y * 8; // 8 tiles per row in blockset
if (selected_tile != current_tile16_ && selected_tile >= 0 &&
selected_tile < 512) {
RETURN_IF_ERROR(SetCurrentTile(selected_tile));
util::logf("Selected Tile16 from blockset: %d (grid: %d,%d)",
selected_tile, grid_x, grid_y);
}
}
blockset_canvas_.DrawBitmap(tile16_blockset_bmp_, 0, true,
blockset_canvas_.GetGlobalScale());
blockset_canvas_.DrawGrid();
blockset_canvas_.DrawOverlay();
EndChild();
return absl::OkStatus();
}
// ROM data access methods
gfx::Tile16* Tile16Editor::GetCurrentTile16Data() {
if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 4096) {
return nullptr;
}
// Read the current tile16 data from ROM
auto tile_result = rom_->ReadTile16(current_tile16_);
if (!tile_result.ok()) {
return nullptr;
}
// Store in instance variable for proper persistence
current_tile16_data_ = tile_result.value();
return &current_tile16_data_;
}
absl::Status Tile16Editor::UpdateROMTile16Data() {
auto* tile_data = GetCurrentTile16Data();
if (!tile_data) {
return absl::FailedPreconditionError("Cannot access current tile16 data");
}
// Write the modified tile16 data back to ROM
RETURN_IF_ERROR(rom_->WriteTile16(current_tile16_, *tile_data));
util::logf("ROM Tile16 data written for tile %d", current_tile16_);
return absl::OkStatus();
}
absl::Status Tile16Editor::RefreshTile16Blockset() {
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not available");
}
// CRITICAL FIX: Force regeneration without using problematic tile cache
// Directly mark atlas as modified to trigger regeneration from ROM data
// Mark atlas as modified to trigger regeneration
tile16_blockset_->atlas.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&tile16_blockset_->atlas);
util::logf("Tile16 blockset refreshed and regenerated");
return absl::OkStatus();
}
absl::Status Tile16Editor::UpdateBlocksetBitmap() {
gfx::ScopedTimer timer("tile16_blockset_update");
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not initialized");
}
if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
return absl::OutOfRangeError("Current tile16 ID out of range");
}
// Use optimized batch operations for better performance
if (tile16_blockset_bmp_.is_active() && current_tile16_bmp_.is_active()) {
// Calculate the position of this tile in the blockset bitmap
constexpr int kTilesPerRow =
8; // Standard SNES tile16 layout is 8 tiles per row
int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
// Use dirty region tracking for efficient updates (region calculated but
// not used in current implementation)
// Copy pixel data from current tile to blockset bitmap using batch
// operations
for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
++tile_x_offset) {
int src_index = tile_y_offset * kTile16Size + tile_x_offset;
int dst_index =
(tile_y + tile_y_offset) * tile16_blockset_bmp_.width() +
(tile_x + tile_x_offset);
if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
dst_index < static_cast<int>(tile16_blockset_bmp_.size())) {
uint8_t pixel_value = current_tile16_bmp_.data()[src_index];
tile16_blockset_bmp_.WriteToPixel(dst_index, pixel_value);
}
}
}
// Mark the blockset bitmap as modified and queue texture update
tile16_blockset_bmp_.set_modified(true);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_bmp_);
// Also update the tile16 blockset atlas if available
if (tile16_blockset_->atlas.is_active()) {
// Update the atlas with the new tile data
for (int tile_y_offset = 0; tile_y_offset < kTile16Size;
++tile_y_offset) {
for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
++tile_x_offset) {
int src_index = tile_y_offset * kTile16Size + tile_x_offset;
int dst_index =
(tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
(tile_x + tile_x_offset);
if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
tile16_blockset_->atlas.WriteToPixel(
dst_index, current_tile16_bmp_.data()[src_index]);
}
}
}
tile16_blockset_->atlas.set_modified(true);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas);
}
}
return absl::OkStatus();
}
absl::Status Tile16Editor::RegenerateTile16BitmapFromROM() {
// Get the current tile16 data from ROM
auto* tile_data = GetCurrentTile16Data();
if (!tile_data) {
return absl::FailedPreconditionError("Cannot access current tile16 data");
}
// Create a new 16x16 bitmap for the tile16
std::vector<uint8_t> tile16_pixels(kTile16PixelCount, 0);
// Process each quadrant (2x2 grid of 8x8 tiles)
for (int quadrant = 0; quadrant < 4; ++quadrant) {
gfx::TileInfo* tile_info = nullptr;
int quadrant_x = quadrant % 2;
int quadrant_y = quadrant / 2;
// Get the tile info for this quadrant
switch (quadrant) {
case 0:
tile_info = &tile_data->tile0_;
break;
case 1:
tile_info = &tile_data->tile1_;
break;
case 2:
tile_info = &tile_data->tile2_;
break;
case 3:
tile_info = &tile_data->tile3_;
break;
}
if (!tile_info)
continue;
// Get the tile8 ID and properties
int tile8_id = tile_info->id_;
bool x_flip = tile_info->horizontal_mirror_;
bool y_flip = tile_info->vertical_mirror_;
// Palette information stored in tile_info but applied via separate palette
// system
// Get the source tile8 bitmap
if (tile8_id >= 0 &&
tile8_id < static_cast<int>(current_gfx_individual_.size()) &&
current_gfx_individual_[tile8_id].is_active()) {
const auto& source_tile8 = current_gfx_individual_[tile8_id];
// Copy the 8x8 tile into the appropriate quadrant of the 16x16 tile
for (int ty = 0; ty < kTile8Size; ++ty) {
for (int tx = 0; tx < kTile8Size; ++tx) {
// Apply flip transformations
int src_x = x_flip ? (kTile8Size - 1 - tx) : tx;
int src_y = y_flip ? (kTile8Size - 1 - ty) : ty;
int src_index = src_y * kTile8Size + src_x;
// Calculate destination in tile16
int dst_x = (quadrant_x * kTile8Size) + tx;
int dst_y = (quadrant_y * kTile8Size) + ty;
int dst_index = dst_y * kTile16Size + dst_x;
// Copy pixel with bounds checking
if (src_index >= 0 &&
src_index < static_cast<int>(source_tile8.size()) &&
dst_index >= 0 && dst_index < kTile16PixelCount) {
uint8_t pixel = source_tile8.data()[src_index];
// Apply palette offset if needed
tile16_pixels[dst_index] = pixel;
}
}
}
}
}
// Update the current tile16 bitmap with the regenerated data
current_tile16_bmp_.Create(kTile16Size, kTile16Size, 8, tile16_pixels);
// Set the appropriate palette using the same system as overworld
if (overworld_palette_.size() >= 256) {
// Use complete 256-color palette (same as overworld system)
// The pixel data already contains correct color indices for the 256-color
// palette
current_tile16_bmp_.SetPalette(overworld_palette_);
} else {
// Fallback to ROM palette
const auto& ow_main_pal_group = rom()->palette_group().overworld_main;
if (ow_main_pal_group.size() > 0) {
current_tile16_bmp_.SetPalette(ow_main_pal_group[0]);
}
}
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&current_tile16_bmp_);
util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
current_tile16_);
return absl::OkStatus();
}
absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
const gfx::Bitmap* source_tile) {
constexpr int kTile8Size = 8;
constexpr int kTile16Size = 16;
// Save undo state before making changes
auto now = std::chrono::steady_clock::now();
auto time_since_last_edit =
std::chrono::duration_cast<std::chrono::milliseconds>(now -
last_edit_time_)
.count();
if (time_since_last_edit > 100) { // 100ms threshold
SaveUndoState();
last_edit_time_ = now;
}
// Validate inputs
if (current_tile8_ < 0 ||
current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
return absl::OutOfRangeError(
absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
}
if (!current_gfx_individual_[current_tile8_].is_active()) {
return absl::FailedPreconditionError("Source tile8 bitmap not active");
}
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("Target tile16 bitmap not active");
}
// Determine which quadrant was clicked - handle the 8x scale factor properly
int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
int start_x = quadrant_x * kTile8Size;
int start_y = quadrant_y * kTile8Size;
// Get source tile8 data - use provided tile if available, otherwise use
// current tile8
const gfx::Bitmap* tile_to_use =
source_tile ? source_tile : &current_gfx_individual_[current_tile8_];
if (tile_to_use->size() < 64) {
return absl::FailedPreconditionError("Source tile data too small");
}
// Copy tile8 into tile16 quadrant with proper transformations
for (int tile_y = 0; tile_y < kTile8Size; ++tile_y) {
for (int tile_x = 0; tile_x < kTile8Size; ++tile_x) {
// Apply flip transformations to source coordinates only if using original
// tile If a pre-flipped tile is provided, use direct coordinates
int src_x;
int src_y;
if (source_tile) {
// Pre-flipped tile provided, use direct coordinates
src_x = tile_x;
src_y = tile_y;
} else {
// Original tile, apply flip transformations
src_x = x_flip ? (kTile8Size - 1 - tile_x) : tile_x;
src_y = y_flip ? (kTile8Size - 1 - tile_y) : tile_y;
}
int src_index = src_y * kTile8Size + src_x;
// Calculate destination in tile16
int dst_x = start_x + tile_x;
int dst_y = start_y + tile_y;
int dst_index = dst_y * kTile16Size + dst_x;
// Bounds check and copy pixel
if (src_index >= 0 && src_index < static_cast<int>(tile_to_use->size()) &&
dst_index >= 0 &&
dst_index < static_cast<int>(current_tile16_bmp_.size())) {
uint8_t pixel_value = tile_to_use->data()[src_index];
// Keep original pixel values - palette selection is handled by TileInfo
// metadata not by modifying pixel data directly
current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
}
}
}
// Mark the bitmap as modified and queue texture update
current_tile16_bmp_.set_modified(true);
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
// Update ROM data when painting to tile16
auto* tile_data = GetCurrentTile16Data();
if (tile_data) {
// Update the quadrant's TileInfo based on current settings
int quadrant_index = quadrant_x + (quadrant_y * 2);
if (quadrant_index >= 0 && quadrant_index < 4) {
// Create new TileInfo with current settings
gfx::TileInfo new_tile_info(static_cast<uint16_t>(current_tile8_),
current_palette_, y_flip, x_flip,
priority_tile);
// Get pointer to the correct quadrant TileInfo
gfx::TileInfo* quadrant_tile = nullptr;
switch (quadrant_index) {
case 0:
quadrant_tile = &tile_data->tile0_;
break;
case 1:
quadrant_tile = &tile_data->tile1_;
break;
case 2:
quadrant_tile = &tile_data->tile2_;
break;
case 3:
quadrant_tile = &tile_data->tile3_;
break;
}
if (quadrant_tile && !(*quadrant_tile == new_tile_info)) {
*quadrant_tile = new_tile_info;
// Update the tiles_info array as well
tile_data->tiles_info[quadrant_index] = new_tile_info;
util::logf(
"Updated ROM Tile16 %d, quadrant %d: Tile8=%d, Pal=%d, XFlip=%d, "
"YFlip=%d, Priority=%d",
current_tile16_, quadrant_index, current_tile8_, current_palette_,
x_flip, y_flip, priority_tile);
}
}
}
// CRITICAL FIX: Don't write to ROM immediately - only update local data
// ROM will be updated when user explicitly clicks "Save to ROM"
// Update the blockset bitmap displayed in the editor (local preview only)
RETURN_IF_ERROR(UpdateBlocksetBitmap());
// Update live preview if enabled (but don't save to ROM)
if (live_preview_enabled_) {
RETURN_IF_ERROR(UpdateOverworldTilemap());
}
util::logf(
"Local tile16 changes made (not saved to ROM yet). Use 'Save to ROM' to "
"commit.");
return absl::OkStatus();
}
absl::Status Tile16Editor::UpdateTile16Edit() {
static bool show_advanced_controls = false;
static bool show_debug_info = false;
// Modern header with improved styling
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 4));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 4));
// Header section with better visual hierarchy
ImGui::BeginGroup();
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tile16 Editor");
ImGui::SameLine();
ImGui::TextDisabled("ID: %02X", current_tile16_);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::TextDisabled("Palette: %d", current_palette_);
// Show actual palette slot for debugging
if (show_debug_info) {
ImGui::SameLine();
int actual_slot = GetActualPaletteSlotForCurrentTile16();
ImGui::TextDisabled("(Slot: %d)", actual_slot);
}
ImGui::EndGroup();
// Modern button styling for controls
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 180);
if (ImGui::Button("Debug Info", ImVec2(80, 0))) {
show_debug_info = !show_debug_info;
}
ImGui::SameLine();
if (ImGui::Button("Advanced", ImVec2(80, 0))) {
show_advanced_controls = !show_advanced_controls;
}
ImGui::PopStyleVar(2);
ImGui::Separator();
// REFACTORED: Improved 3-column layout with better space utilization
if (ImGui::BeginTable("##Tile16EditLayout", 3,
ImGuiTableFlags_Resizable |
ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Tile16 Blockset",
ImGuiTableColumnFlags_WidthStretch, 0.35f);
ImGui::TableSetupColumn("Tile8 Source", ImGuiTableColumnFlags_WidthStretch,
0.35f);
ImGui::TableSetupColumn("Editor & Controls",
ImGuiTableColumnFlags_WidthStretch, 0.30f);
ImGui::TableHeadersRow();
ImGui::TableNextRow();
// ========== COLUMN 1: Tile16 Blockset ==========
ImGui::TableNextColumn();
ImGui::BeginGroup();
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile16 Blockset");
// Blockset canvas with scrolling
if (BeginChild("##BlocksetScrollable",
ImVec2(0, ImGui::GetContentRegionAvail().y), true,
ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
blockset_canvas_.DrawBackground();
blockset_canvas_.DrawContextMenu();
// Handle tile selection from blockset
bool tile_selected = false;
blockset_canvas_.DrawTileSelector(32.0f);
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
blockset_canvas_.IsMouseHovering()) {
tile_selected = true;
}
if (tile_selected) {
const ImGuiIO& io = ImGui::GetIO();
ImVec2 canvas_pos = blockset_canvas_.zero_point();
ImVec2 mouse_pos =
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
int grid_x = static_cast<int>(mouse_pos.x /
(32 * blockset_canvas_.GetGlobalScale()));
int grid_y = static_cast<int>(mouse_pos.y /
(32 * blockset_canvas_.GetGlobalScale()));
int selected_tile = grid_x + grid_y * 8;
if (selected_tile != current_tile16_ && selected_tile >= 0) {
RETURN_IF_ERROR(SetCurrentTile(selected_tile));
util::logf("Selected Tile16 from blockset: %d", selected_tile);
}
}
blockset_canvas_.DrawBitmap(tile16_blockset_bmp_, 0, true, 2);
blockset_canvas_.DrawGrid();
blockset_canvas_.DrawOverlay();
}
EndChild();
ImGui::EndGroup();
// ========== COLUMN 2: Tile8 Source ==========
ImGui::TableNextColumn();
ImGui::BeginGroup();
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile8 Source");
tile8_source_canvas_.set_draggable(false);
// Scrollable tile8 source
if (BeginChild("##Tile8SourceScrollable", ImVec2(0, 0), true,
ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
tile8_source_canvas_.DrawBackground();
tile8_source_canvas_.DrawContextMenu();
// Tile8 selection with improved feedback
bool tile8_selected = false;
tile8_source_canvas_.DrawTileSelector(32.0F);
// Check for clicks properly
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
tile8_selected = true;
}
if (tile8_selected) {
const ImGuiIO& io = ImGui::GetIO();
ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
ImVec2 mouse_pos =
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
// Account for dynamic zoom when calculating tile position
int tile_x = static_cast<int>(
mouse_pos.x /
(8 * 4)); // 8 pixel tile * 4x scale = 32 pixels per tile
int tile_y = static_cast<int>(mouse_pos.y / (8 * 4));
// Calculate tiles per row based on bitmap width
int tiles_per_row = current_gfx_bmp_.width() / 8;
int new_tile8 = tile_x + (tile_y * tiles_per_row);
if (new_tile8 != current_tile8_ && new_tile8 >= 0 &&
new_tile8 < static_cast<int>(current_gfx_individual_.size()) &&
current_gfx_individual_[new_tile8].is_active()) {
current_tile8_ = new_tile8;
RETURN_IF_ERROR(UpdateTile8Palette(current_tile8_));
util::logf("Selected Tile8: %d", current_tile8_);
}
}
tile8_source_canvas_.DrawBitmap(current_gfx_bmp_, 2, 2, 4);
tile8_source_canvas_.DrawGrid();
tile8_source_canvas_.DrawOverlay();
}
EndChild();
ImGui::EndGroup();
// ========== COLUMN 3: Tile16 Editor + Controls ==========
TableNextColumn();
ImGui::BeginGroup();
// Fixed size container to prevent canvas expansion
if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse)) {
tile16_edit_canvas_.DrawBackground(ImVec2(64, 64));
tile16_edit_canvas_.DrawContextMenu();
// Draw current tile16 bitmap with dynamic zoom
if (current_tile16_bmp_.is_active()) {
tile16_edit_canvas_.DrawBitmap(current_tile16_bmp_, 2, 2, 4);
}
// Handle tile8 painting with improved hover preview
if (current_tile8_ >= 0 &&
current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
current_gfx_individual_[current_tile8_].is_active()) {
// Create a display tile that shows the current palette selection
gfx::Bitmap display_tile;
// Get the original pixel data (already has sheet offsets from
// ProcessGraphicsBuffer)
std::vector<uint8_t> tile_data =
current_gfx_individual_[current_tile8_].vector();
// The pixel data already contains the correct indices for the 256-color
// palette We don't need to remap - just use it as-is
display_tile.Create(8, 8, 8, tile_data);
// Apply the complete 256-color palette
if (overworld_palette_.size() >= 256) {
display_tile.SetPalette(overworld_palette_);
} else {
display_tile.SetPalette(
current_gfx_individual_[current_tile8_].palette());
}
// Apply flips if needed
if (x_flip || y_flip) {
auto& data = display_tile.mutable_data();
if (x_flip) {
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 4; ++x) {
std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
}
}
}
if (y_flip) {
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 8; ++x) {
std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
}
}
}
}
// Queue texture creation for display tile
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &display_tile);
// CRITICAL FIX: Handle tile painting with simple click instead of
// click+drag Draw the preview first
tile16_edit_canvas_.DrawTilePainter(display_tile, 8, 4);
// Check for simple click to paint tile8 to tile16
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
const ImGuiIO& io = ImGui::GetIO();
ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
io.MousePos.y - canvas_pos.y);
// Convert canvas coordinates to tile16 coordinates with dynamic zoom
int tile_x = static_cast<int>(mouse_pos.x / 4);
int tile_y = static_cast<int>(mouse_pos.y / 4);
// Clamp to valid range
tile_x = std::max(0, std::min(15, tile_x));
tile_y = std::max(0, std::min(15, tile_y));
util::logf("Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d)",
mouse_pos.x, mouse_pos.y, tile_x, tile_y);
RETURN_IF_ERROR(
DrawToCurrentTile16(ImVec2(tile_x, tile_y), &display_tile));
}
// CRITICAL FIX: Right-click to pick tile8 from tile16
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
const ImGuiIO& io = ImGui::GetIO();
ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
io.MousePos.y - canvas_pos.y);
// Convert with dynamic zoom
int tile_x = static_cast<int>(mouse_pos.x / 4);
int tile_y = static_cast<int>(mouse_pos.y / 4);
// Clamp to valid range
tile_x = std::max(0, std::min(15, tile_x));
tile_y = std::max(0, std::min(15, tile_y));
RETURN_IF_ERROR(PickTile8FromTile16(ImVec2(tile_x, tile_y)));
util::logf("Right-clicked to pick tile8 from tile16 at (%d, %d)",
tile_x, tile_y);
}
}
tile16_edit_canvas_.DrawGrid(8.0F); // Scale grid with zoom
tile16_edit_canvas_.DrawOverlay();
}
ImGui::EndChild();
Separator();
// === Compact Controls Section ===
// Tile8 info and preview
if (current_tile8_ >= 0 &&
current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
current_gfx_individual_[current_tile8_].is_active()) {
Text("Tile8: %02X", current_tile8_);
SameLine();
auto* tile8_texture = current_gfx_individual_[current_tile8_].texture();
if (tile8_texture) {
ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
}
}
// Tile8 transform options in compact form
Checkbox("X Flip", &x_flip);
SameLine();
Checkbox("Y Flip", &y_flip);
SameLine();
Checkbox("Priority", &priority_tile);
Separator();
// Palette selector - more compact
Text("Palette:");
if (show_debug_info) {
SameLine();
int actual_slot = GetActualPaletteSlotForCurrentTile16();
ImGui::TextDisabled("(Slot %d)", actual_slot);
}
// Compact palette grid
ImGui::BeginGroup();
float available_width = ImGui::GetContentRegionAvail().x;
float button_size = std::min(32.0f, (available_width - 16.0f) / 4.0f);
for (int row = 0; row < 2; ++row) {
for (int col = 0; col < 4; ++col) {
if (col > 0)
ImGui::SameLine();
int i = row * 4 + col;
bool is_current = (current_palette_ == i);
// Modern button styling with better visual hierarchy
ImGui::PushID(i);
if (is_current) {
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(0.2f, 0.7f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.3f, 0.8f, 0.4f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.1f, 0.6f, 0.2f, 1.0f));
} else {
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(0.3f, 0.3f, 0.35f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.4f, 0.4f, 0.45f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.25f, 0.25f, 0.3f, 1.0f));
}
// Add border for better definition
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Border,
is_current ? ImVec4(0.4f, 0.9f, 0.5f, 1.0f)
: ImVec4(0.5f, 0.5f, 0.5f, 0.3f));
if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
ImVec2(button_size, button_size))) {
if (current_palette_ != i) {
current_palette_ = i;
auto status = RefreshAllPalettes();
if (!status.ok()) {
util::logf("Failed to refresh palettes: %s",
status.message().data());
} else {
util::logf("Palette successfully changed to %d",
current_palette_);
}
}
}
ImGui::PopStyleColor(4); // 3 button colors + 1 border color
ImGui::PopStyleVar(1); // border size
ImGui::PopID();
// Simplified tooltip
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (show_debug_info) {
ImGui::Text("Palette %d → Slots:", i);
ImGui::Text(" S0,3,4: %d", GetActualPaletteSlot(i, 0));
ImGui::Text(" S1,2: %d", GetActualPaletteSlot(i, 1));
ImGui::Text(" S5,6: %d", GetActualPaletteSlot(i, 5));
ImGui::Text(" S7: %d", GetActualPaletteSlot(i, 7));
} else {
ImGui::Text("Palette %d", i);
if (is_current) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Active");
}
}
ImGui::EndTooltip();
}
}
}
ImGui::EndGroup();
Separator();
// Compact action buttons
if (Button("Clear", ImVec2(-1, 0))) {
RETURN_IF_ERROR(ClearTile16());
}
if (Button("Copy", ImVec2(-1, 0))) {
RETURN_IF_ERROR(CopyTile16ToClipboard(current_tile16_));
}
if (Button("Paste", ImVec2(-1, 0))) {
RETURN_IF_ERROR(PasteTile16FromClipboard());
}
Separator();
// Save/Discard - full width buttons
if (Button("Save Changes", ImVec2(-1, 0))) {
RETURN_IF_ERROR(CommitChangesToOverworld());
}
HOVER_HINT("Apply changes to overworld and regenerate blockset");
if (Button("Discard Changes", ImVec2(-1, 0))) {
RETURN_IF_ERROR(DiscardChanges());
}
HOVER_HINT("Reload tile16 from ROM, discarding local changes");
Separator();
bool can_undo = !undo_stack_.empty();
if (!can_undo)
BeginDisabled();
if (Button("Undo", ImVec2(-1, 0))) {
RETURN_IF_ERROR(Undo());
}
if (!can_undo)
EndDisabled();
// Advanced controls (collapsible)
if (show_advanced_controls) {
Separator();
Text("Advanced:");
if (Button("Palette Settings", ImVec2(-1, 0))) {
show_palette_settings_ = !show_palette_settings_;
}
if (Button("Manual Edit", ImVec2(-1, 0))) {
ImGui::OpenPopup("ManualTile8Editor");
}
if (Button("Refresh Blockset", ImVec2(-1, 0))) {
RETURN_IF_ERROR(RefreshTile16Blockset());
}
// Scratch space in compact form
Text("Scratch:");
DrawScratchSpace();
// Manual tile8 editor popup
DrawManualTile8Inputs();
}
// Compact debug information panel
if (show_debug_info) {
Separator();
Text("Debug:");
ImGui::TextDisabled("T16:%02X T8:%d Pal:%d", current_tile16_,
current_tile8_, current_palette_);
if (current_tile8_ >= 0) {
int sheet_index = GetSheetIndexForTile8(current_tile8_);
int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
ImGui::TextDisabled("Sheet:%d Slot:%d", sheet_index, actual_slot);
}
// Compact palette mapping table
if (ImGui::CollapsingHeader("Palette Map",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
if (ImGui::BeginTable("##PalMap", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
ImGui::TableSetupColumn("S0,3-4", ImGuiTableColumnFlags_WidthFixed,
50);
ImGui::TableSetupColumn("S1-2", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableHeadersRow();
for (int i = 0; i < 8; ++i) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", i);
ImGui::TableNextColumn();
ImGui::Text("%d", GetActualPaletteSlot(i, 0));
ImGui::TableNextColumn();
ImGui::Text("%d", GetActualPaletteSlot(i, 1));
}
ImGui::EndTable();
}
ImGui::EndChild();
}
// Color preview - compact
if (ImGui::CollapsingHeader("Colors")) {
if (overworld_palette_.size() >= 256) {
int actual_slot = GetActualPaletteSlotForCurrentTile16();
ImGui::Text("Slot %d:", actual_slot);
for (int i = 0;
i < 8 &&
(actual_slot + i) < static_cast<int>(overworld_palette_.size());
++i) {
int color_index = actual_slot + i;
auto color = overworld_palette_[color_index];
ImVec4 display_color = color.rgb();
ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
display_color, ImGuiColorEditFlags_NoTooltip,
ImVec2(20, 20));
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%d:0x%04X", color_index, color.snes());
}
if ((i + 1) % 4 != 0)
ImGui::SameLine();
}
}
}
}
ImGui::EndGroup();
EndTable();
}
// Draw palette settings and canvas popups
DrawPaletteSettings();
// Show canvas popup windows if opened from context menu
blockset_canvas_.ShowAdvancedCanvasProperties();
blockset_canvas_.ShowScalingControls();
tile8_source_canvas_.ShowAdvancedCanvasProperties();
tile8_source_canvas_.ShowScalingControls();
tile16_edit_canvas_.ShowAdvancedCanvasProperties();
tile16_edit_canvas_.ShowScalingControls();
return absl::OkStatus();
}
absl::Status Tile16Editor::LoadTile8() {
if (!current_gfx_bmp_.is_active() || current_gfx_bmp_.data() == nullptr) {
return absl::FailedPreconditionError(
"Current graphics bitmap not initialized");
}
current_gfx_individual_.clear();
// Calculate how many 8x8 tiles we can fit based on the current graphics
// bitmap size SNES graphics are typically 128 pixels wide (16 tiles of 8
// pixels each)
const int tiles_per_row = current_gfx_bmp_.width() / 8;
const int total_rows = current_gfx_bmp_.height() / 8;
const int total_tiles = tiles_per_row * total_rows;
current_gfx_individual_.reserve(total_tiles);
// Extract individual 8x8 tiles from the graphics bitmap
for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
std::vector<uint8_t> tile_data(64); // 8x8 = 64 pixels
// Extract tile data from the main graphics bitmap
// Keep raw 4-bit pixel values (0-15); palette offset is applied in
// RefreshAllPalettes() via SetPaletteWithTransparent
for (int py = 0; py < 8; ++py) {
for (int px = 0; px < 8; ++px) {
int src_x = tile_x * 8 + px;
int src_y = tile_y * 8 + py;
int src_index = src_y * current_gfx_bmp_.width() + src_x;
int dst_index = py * 8 + px;
if (src_index < static_cast<int>(current_gfx_bmp_.size()) &&
dst_index < 64) {
uint8_t pixel_value = current_gfx_bmp_.data()[src_index];
// Normalize to 4-bit range for proper SNES 4bpp graphics
// The actual palette offset is applied during palette refresh
pixel_value &= 0x0F;
tile_data[dst_index] = pixel_value;
}
}
}
// Create the individual tile bitmap
current_gfx_individual_.emplace_back();
auto& tile_bitmap = current_gfx_individual_.back();
try {
tile_bitmap.Create(8, 8, 8, tile_data);
// Set default palette using the same system as overworld
if (overworld_palette_.size() >= 256) {
// Use complete 256-color palette (same as overworld system)
// The pixel data already contains correct color indices for the
// 256-color palette
tile_bitmap.SetPalette(overworld_palette_);
} else if (rom() && rom()->palette_group().overworld_main.size() > 0) {
// Fallback to ROM palette
tile_bitmap.SetPalette(rom()->palette_group().overworld_main[0]);
}
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &tile_bitmap);
} catch (const std::exception& e) {
util::logf("Error creating tile at (%d,%d): %s", tile_x, tile_y,
e.what());
// Create an empty bitmap as fallback
tile_bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
}
}
}
// Apply current palette settings to all tiles
if (rom_) {
RETURN_IF_ERROR(RefreshAllPalettes());
}
util::logf("Loaded %zu individual tile8 graphics",
current_gfx_individual_.size());
return absl::OkStatus();
}
absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
return absl::OutOfRangeError(
absl::StrFormat("Invalid tile16 id: %d", tile_id));
}
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not initialized");
}
current_tile16_ = tile_id;
// Initialize the instance variable with current ROM data
auto tile_result = rom_->ReadTile16(current_tile16_);
if (tile_result.ok()) {
current_tile16_data_ = tile_result.value();
}
// Extract tile data using the same method as GetTilemapData
auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
if (tile_data.empty()) {
// If GetTilemapData fails, manually extract from the atlas
const int kTilesPerRow = 8; // Standard tile16 blockset layout
int tile_x = (tile_id % kTilesPerRow) * kTile16Size;
int tile_y = (tile_id / kTilesPerRow) * kTile16Size;
tile_data.resize(kTile16PixelCount);
// Manual extraction - preserve pixel values for palette-based rendering
// The 4-bit mask is applied after extraction to normalize values
for (int ty = 0; ty < kTile16Size; ty++) {
for (int tx = 0; tx < kTile16Size; tx++) {
int pixel_x = tile_x + tx;
int pixel_y = tile_y + ty;
int src_index = (pixel_y * tile16_blockset_->atlas.width()) + pixel_x;
int dst_index = ty * kTile16Size + tx;
if (src_index < static_cast<int>(tile16_blockset_->atlas.size()) &&
dst_index < static_cast<int>(tile_data.size())) {
uint8_t pixel_value = tile16_blockset_->atlas.data()[src_index];
// Normalize pixel values to 4-bit range for sub-palette indexing
// The actual palette offset is applied via SetPaletteWithTransparent
pixel_value &= 0x0F;
tile_data[dst_index] = pixel_value;
}
}
}
} else {
// Normalize the extracted data to 4-bit range
for (auto& pixel : tile_data) {
pixel &= 0x0F;
}
}
// Create the bitmap with the extracted data
current_tile16_bmp_.Create(kTile16Size, kTile16Size, 8, tile_data);
// CRITICAL FIX: Use SetPaletteWithTransparent with proper palette offset
// based on current_palette_ selection and default sheet (sheet 0 for tile16)
gfx::SnesPalette display_palette;
if (overworld_palette_.size() >= 256) {
display_palette = overworld_palette_;
} else if (palette_.size() >= 256) {
display_palette = palette_;
} else if (rom()->palette_group().overworld_main.size() > 0) {
display_palette = rom()->palette_group().overworld_main[0];
}
// Calculate palette offset: use sheet 0 (main blockset) as default for tile16
// palette_base * 16 gives the row offset, current_palette_ * 8 gives
// sub-palette
int palette_base = GetPaletteBaseForSheet(0); // Default to main blockset
size_t palette_offset = (palette_base * 16) + (current_palette_ * 8);
// Defensive checks: ensure palette is present and offset is valid
if (display_palette.empty()) {
util::logf("Tile16Editor: display palette empty; falling back to offset 0");
return absl::FailedPreconditionError("display palette unavailable");
}
if (palette_offset + 7 >= display_palette.size()) {
util::logf("Tile16Editor: palette offset %zu out of range (size=%zu); "
"using offset 0",
palette_offset, display_palette.size());
palette_offset = 0;
if (display_palette.size() < 8) {
return absl::FailedPreconditionError("display palette too small");
}
}
// Apply the correct sub-palette with transparency
current_tile16_bmp_.SetPaletteWithTransparent(display_palette, palette_offset,
7);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&current_tile16_bmp_);
// Simple success logging
util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
return absl::OkStatus();
}
absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
return absl::InvalidArgumentError("Invalid tile ID");
}
// CRITICAL FIX: Extract tile data directly from atlas instead of using
// problematic tile cache
auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
if (!tile_data.empty()) {
clipboard_tile16_.Create(16, 16, 8, tile_data);
clipboard_tile16_.SetPalette(tile16_blockset_->atlas.palette());
}
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&clipboard_tile16_);
clipboard_has_data_ = true;
return absl::OkStatus();
}
absl::Status Tile16Editor::PasteTile16FromClipboard() {
if (!clipboard_has_data_) {
return absl::FailedPreconditionError("Clipboard is empty");
}
// Copy the clipboard data to the current tile16
current_tile16_bmp_.Create(16, 16, 8, clipboard_tile16_.vector());
current_tile16_bmp_.SetPalette(clipboard_tile16_.palette());
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::SaveTile16ToScratchSpace(int slot) {
if (slot < 0 || slot >= 4) {
return absl::InvalidArgumentError("Invalid scratch space slot");
}
// Create a copy of the current tile16 bitmap
scratch_space_[slot].Create(16, 16, 8, current_tile16_bmp_.vector());
scratch_space_[slot].SetPalette(current_tile16_bmp_.palette());
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&scratch_space_[slot]);
scratch_space_used_[slot] = true;
return absl::OkStatus();
}
absl::Status Tile16Editor::LoadTile16FromScratchSpace(int slot) {
if (slot < 0 || slot >= 4) {
return absl::InvalidArgumentError("Invalid scratch space slot");
}
if (!scratch_space_used_[slot]) {
return absl::FailedPreconditionError("Scratch space slot is empty");
}
// Copy the scratch space data to the current tile16
current_tile16_bmp_.Create(16, 16, 8, scratch_space_[slot].vector());
current_tile16_bmp_.SetPalette(scratch_space_[slot].palette());
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::ClearScratchSpace(int slot) {
if (slot < 0 || slot >= 4) {
return absl::InvalidArgumentError("Invalid scratch space slot");
}
scratch_space_used_[slot] = false;
return absl::OkStatus();
}
// Advanced editing features
absl::Status Tile16Editor::FlipTile16Horizontal() {
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("No active tile16 to flip");
}
SaveUndoState();
// Create a temporary bitmap for the flipped result
gfx::Bitmap flipped_bitmap;
flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
// Flip horizontally by copying pixels in reverse x order
for (int y = 0; y < 16; ++y) {
for (int x = 0; x < 16; ++x) {
int src_index = y * 16 + x;
int dst_index = y * 16 + (15 - x);
if (src_index < current_tile16_bmp_.size() &&
dst_index < flipped_bitmap.size()) {
flipped_bitmap.WriteToPixel(dst_index,
current_tile16_bmp_.data()[src_index]);
}
}
}
// Copy the flipped result back
current_tile16_bmp_ = std::move(flipped_bitmap);
current_tile16_bmp_.SetPalette(palette_);
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::FlipTile16Vertical() {
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("No active tile16 to flip");
}
SaveUndoState();
// Create a temporary bitmap for the flipped result
gfx::Bitmap flipped_bitmap;
flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
// Flip vertically by copying pixels in reverse y order
for (int y = 0; y < 16; ++y) {
for (int x = 0; x < 16; ++x) {
int src_index = y * 16 + x;
int dst_index = (15 - y) * 16 + x;
if (src_index < current_tile16_bmp_.size() &&
dst_index < flipped_bitmap.size()) {
flipped_bitmap.WriteToPixel(dst_index,
current_tile16_bmp_.data()[src_index]);
}
}
}
// Copy the flipped result back
current_tile16_bmp_ = std::move(flipped_bitmap);
current_tile16_bmp_.SetPalette(palette_);
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::RotateTile16() {
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("No active tile16 to rotate");
}
SaveUndoState();
// Create a temporary bitmap for the rotated result
gfx::Bitmap rotated_bitmap;
rotated_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
// Rotate 90 degrees clockwise
for (int y = 0; y < 16; ++y) {
for (int x = 0; x < 16; ++x) {
int src_index = y * 16 + x;
int dst_index = x * 16 + (15 - y);
if (src_index < current_tile16_bmp_.size() &&
dst_index < rotated_bitmap.size()) {
rotated_bitmap.WriteToPixel(dst_index,
current_tile16_bmp_.data()[src_index]);
}
}
}
// Copy the rotated result back
current_tile16_bmp_ = std::move(rotated_bitmap);
current_tile16_bmp_.SetPalette(palette_);
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
if (tile8_id < 0 ||
tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
return absl::InvalidArgumentError("Invalid tile8 ID");
}
if (!current_gfx_individual_[tile8_id].is_active()) {
return absl::FailedPreconditionError("Source tile8 not active");
}
SaveUndoState();
// Fill all four quadrants with the same tile8
for (int quadrant = 0; quadrant < 4; ++quadrant) {
int start_x = (quadrant % 2) * 8;
int start_y = (quadrant / 2) * 8;
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
int src_index = y * 8 + x;
int dst_index = (start_y + y) * 16 + (start_x + x);
if (src_index < current_gfx_individual_[tile8_id].size() &&
dst_index < current_tile16_bmp_.size()) {
uint8_t pixel_value =
current_gfx_individual_[tile8_id].data()[src_index];
current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
}
}
}
}
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
absl::Status Tile16Editor::ClearTile16() {
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("No active tile16 to clear");
}
SaveUndoState();
// Fill with transparent/background color (0)
auto& data = current_tile16_bmp_.mutable_data();
std::fill(data.begin(), data.end(), 0);
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
return absl::OkStatus();
}
// Palette management
absl::Status Tile16Editor::CyclePalette(bool forward) {
uint8_t new_palette = current_palette_;
if (forward) {
new_palette = (new_palette + 1) % 8;
} else {
new_palette = (new_palette == 0) ? 7 : new_palette - 1;
}
current_palette_ = new_palette;
// Use the RefreshAllPalettes method which handles all the coordination
RETURN_IF_ERROR(RefreshAllPalettes());
util::logf("Cycled to palette slot %d", current_palette_);
return absl::OkStatus();
}
absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
if (!show_palette_preview_) {
return absl::OkStatus();
}
if (palette_id >= 8) {
return absl::InvalidArgumentError("Invalid palette ID");
}
// Create a preview bitmap with the new palette
if (!preview_tile16_.is_active()) {
preview_tile16_.Create(16, 16, 8, current_tile16_bmp_.vector());
} else {
// Recreate the preview bitmap with new data
preview_tile16_.Create(16, 16, 8, current_tile16_bmp_.vector());
}
const auto& ow_main_pal_group = rom()->palette_group().overworld_main;
if (ow_main_pal_group.size() > palette_id) {
preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0], palette_id);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &preview_tile16_);
preview_dirty_ = true;
}
return absl::OkStatus();
}
// Undo/Redo system
void Tile16Editor::SaveUndoState() {
if (!current_tile16_bmp_.is_active()) {
return;
}
UndoState state;
state.tile_id = current_tile16_;
state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
state.tile_bitmap.SetPalette(current_tile16_bmp_.palette());
state.palette = current_palette_;
state.x_flip = x_flip;
state.y_flip = y_flip;
state.priority = priority_tile;
undo_stack_.push_back(std::move(state));
// Limit undo stack size
if (undo_stack_.size() > kMaxUndoStates_) {
undo_stack_.erase(undo_stack_.begin());
}
// Clear redo stack when new action is performed
redo_stack_.clear();
}
absl::Status Tile16Editor::Undo() {
if (undo_stack_.empty()) {
return absl::FailedPreconditionError("Nothing to undo");
}
// Save current state to redo stack
UndoState current_state;
current_state.tile_id = current_tile16_;
current_state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
current_state.tile_bitmap.SetPalette(current_tile16_bmp_.palette());
current_state.palette = current_palette_;
current_state.x_flip = x_flip;
current_state.y_flip = y_flip;
current_state.priority = priority_tile;
redo_stack_.push_back(std::move(current_state));
// Restore previous state
const UndoState& previous_state = undo_stack_.back();
current_tile16_ = previous_state.tile_id;
current_tile16_bmp_ = previous_state.tile_bitmap;
current_palette_ = previous_state.palette;
x_flip = previous_state.x_flip;
y_flip = previous_state.y_flip;
priority_tile = previous_state.priority;
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
undo_stack_.pop_back();
return absl::OkStatus();
}
absl::Status Tile16Editor::Redo() {
if (redo_stack_.empty()) {
return absl::FailedPreconditionError("Nothing to redo");
}
// Save current state to undo stack
SaveUndoState();
// Restore next state
const UndoState& next_state = redo_stack_.back();
current_tile16_ = next_state.tile_id;
current_tile16_bmp_ = next_state.tile_bitmap;
current_palette_ = next_state.palette;
x_flip = next_state.x_flip;
y_flip = next_state.y_flip;
priority_tile = next_state.priority;
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_tile16_bmp_);
redo_stack_.pop_back();
return absl::OkStatus();
}
absl::Status Tile16Editor::ValidateTile16Data() {
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not initialized");
}
if (current_tile16_ < 0 ||
current_tile16_ >= static_cast<int>(tile16_blockset_->atlas.size())) {
return absl::OutOfRangeError("Current tile16 ID out of range");
}
if (current_palette_ >= 8) {
return absl::OutOfRangeError("Current palette ID out of range");
}
return absl::OkStatus();
}
bool Tile16Editor::IsTile16Valid(int tile_id) const {
return tile_id >= 0 && tile16_blockset_ &&
tile_id < static_cast<int>(tile16_blockset_->atlas.size());
}
// Integration with overworld system
absl::Status Tile16Editor::SaveTile16ToROM() {
if (!rom_) {
return absl::FailedPreconditionError("ROM not available");
}
if (!current_tile16_bmp_.is_active()) {
return absl::FailedPreconditionError("No active tile16 to save");
}
// Update the tile16 blockset with current changes
RETURN_IF_ERROR(UpdateOverworldTilemap());
// Commit changes to the tile16 blockset
RETURN_IF_ERROR(CommitChangesToBlockset());
// Mark ROM as dirty to ensure saving
return absl::OkStatus();
}
absl::Status Tile16Editor::UpdateOverworldTilemap() {
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not initialized");
}
if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
return absl::OutOfRangeError("Current tile16 ID out of range");
}
// CRITICAL FIX: Update atlas directly instead of using problematic tile cache
// This prevents the move-related crashes we experienced earlier
// Update the atlas if needed
if (tile16_blockset_->atlas.is_active()) {
// Update the portion of the atlas that corresponds to this tile
constexpr int kTilesPerRow =
8; // Standard SNES tile16 layout is 8 tiles per row
int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
// Copy pixel data from current tile to atlas
for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
++tile_x_offset) {
int src_index = tile_y_offset * kTile16Size + tile_x_offset;
int dst_index =
(tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
(tile_x + tile_x_offset);
if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
tile16_blockset_->atlas.WriteToPixel(
dst_index, current_tile16_bmp_.data()[src_index]);
}
}
}
tile16_blockset_->atlas.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas);
}
return absl::OkStatus();
}
absl::Status Tile16Editor::CommitChangesToBlockset() {
if (!tile16_blockset_) {
return absl::FailedPreconditionError("Tile16 blockset not initialized");
}
// Regenerate the tilemap data if needed
if (tile16_blockset_->atlas.modified()) {
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas);
}
// Update individual cached tiles
// Note: With the new tile cache system, tiles are automatically managed
// and don't need manual modification tracking like the old system
// The cache handles LRU eviction and automatic updates
return absl::OkStatus();
}
absl::Status Tile16Editor::CommitChangesToOverworld() {
// CRITICAL FIX: Complete workflow for tile16 changes
// This method now only commits to ROM when explicitly called (user presses
// Save)
// Step 1: Update ROM data with current tile16 changes
RETURN_IF_ERROR(UpdateROMTile16Data());
// Step 2: Update the local blockset to reflect changes
RETURN_IF_ERROR(UpdateBlocksetBitmap());
// Step 3: Update the atlas directly (bypass problematic tile cache)
if (tile16_blockset_->atlas.is_active()) {
// Calculate the position of this tile in the blockset atlas
constexpr int kTilesPerRow = 8;
int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
// Copy current tile16 bitmap data directly to atlas
for (int ty = 0; ty < kTile16Size; ++ty) {
for (int tx = 0; tx < kTile16Size; ++tx) {
int src_index = ty * kTile16Size + tx;
int dst_index =
(tile_y + ty) * tile16_blockset_->atlas.width() + (tile_x + tx);
if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
tile16_blockset_->atlas.WriteToPixel(
dst_index, current_tile16_bmp_.data()[src_index]);
}
}
}
tile16_blockset_->atlas.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas);
}
// Step 4: Notify the parent editor (overworld editor) to regenerate its
// blockset
if (on_changes_committed_) {
RETURN_IF_ERROR(on_changes_committed_());
}
util::logf("Committed Tile16 %d changes to overworld system",
current_tile16_);
return absl::OkStatus();
}
absl::Status Tile16Editor::DiscardChanges() {
// Reload the current tile16 from ROM to discard any local changes
RETURN_IF_ERROR(SetCurrentTile(current_tile16_));
util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
return absl::OkStatus();
}
absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
// Get the current tile16 data from ROM
if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 512) {
return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
}
// Determine which quadrant of the tile16 was clicked
int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
// Get the tile16 data structure
auto* tile16_data = GetCurrentTile16Data();
if (!tile16_data) {
return absl::FailedPreconditionError("Failed to get tile16 data");
}
// Extract the tile8 ID from the appropriate quadrant
gfx::TileInfo tile_info;
switch (quadrant) {
case 0:
tile_info = tile16_data->tile0_;
break; // Top-left
case 1:
tile_info = tile16_data->tile1_;
break; // Top-right
case 2:
tile_info = tile16_data->tile2_;
break; // Bottom-left
case 3:
tile_info = tile16_data->tile3_;
break; // Bottom-right
}
// Set the current tile8 and palette
current_tile8_ = tile_info.id_;
current_palette_ = tile_info.palette_;
// Update the flip states based on the tile info
x_flip = tile_info.horizontal_mirror_;
y_flip = tile_info.vertical_mirror_;
priority_tile = tile_info.over_;
// Refresh the palette to match the picked tile
RETURN_IF_ERROR(UpdateTile8Palette(current_tile8_));
RETURN_IF_ERROR(RefreshAllPalettes());
util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
current_tile8_, current_palette_, quadrant, current_tile16_);
return absl::OkStatus();
}
// Get the appropriate palette slot for current graphics sheet
int Tile16Editor::GetPaletteSlotForSheet(int sheet_index) const {
// Based on ProcessGraphicsBuffer logic and overworld palette coordination:
// Sheets 0,3-6: Use AUX palettes (slots 10-15 in 256-color palette)
// Sheets 1-2: Use MAIN palette (slots 2-6 in 256-color palette)
// Sheet 7: Use ANIMATED palette (slot 7 in 256-color palette)
switch (sheet_index) {
case 0:
return 10; // Main blockset -> AUX1 palette region
case 1:
return 2; // Main graphics -> MAIN palette region
case 2:
return 3; // Main graphics -> MAIN palette region
case 3:
return 11; // Area graphics -> AUX1 palette region
case 4:
return 12; // Area graphics -> AUX1 palette region
case 5:
return 13; // Area graphics -> AUX2 palette region
case 6:
return 14; // Area graphics -> AUX2 palette region
case 7:
return 7; // Animated tiles -> ANIMATED palette region
default:
return static_cast<int>(
current_palette_); // Use current selection for other sheets
}
}
// NEW: Get the actual palette slot for a given palette button and sheet index
int Tile16Editor::GetActualPaletteSlot(int palette_button,
int sheet_index) const {
// Map palette buttons 0-7 to actual 256-color palette slots based on sheet
// type Based on the correct 256-color palette structure from
// SetColorsPalette() The 256-color palette is organized as a 16x16 grid (16
// colors per row)
switch (sheet_index) {
case 0: // Main blockset -> AUX1 region (right side, rows 2-4, cols 9-15)
case 3:
case 4:
// AUX1 palette: Row 2-4, cols 9-15 = slots 41-47, 57-63, 73-79
// Use row 2, col 9 + palette_button offset
return 41 + palette_button; // Row 2, col 9 = slot 41
case 5:
case 6: // Area graphics -> AUX2 region (right side, rows 5-7, cols 9-15)
// AUX2 palette: Row 5-7, cols 9-15 = slots 89-95, 105-111, 121-127
// Use row 5, col 9 + palette_button offset
return 89 + palette_button; // Row 5, col 9 = slot 89
case 1:
case 2: // Main graphics -> MAIN region (left side, rows 2-6, cols 1-7)
// MAIN palette: Row 2-6, cols 1-7 = slots 33-39, 49-55, 65-71, 81-87,
// 97-103 Use row 2, col 1 + palette_button offset
return 33 + palette_button; // Row 2, col 1 = slot 33
case 7: // Animated tiles -> ANIMATED region (row 7, cols 1-7)
// ANIMATED palette: Row 7, cols 1-7 = slots 113-119
return 113 + palette_button; // Row 7, col 1 = slot 113
default:
return 33 + palette_button; // Default to MAIN region
}
}
// NEW: Get the sheet index for a given tile8 ID
int Tile16Editor::GetSheetIndexForTile8(int tile8_id) const {
// Determine which graphics sheet a tile8 belongs to based on its position
// This is based on the 256-tile per sheet organization
constexpr int kTilesPerSheet = 256; // 16x16 tiles per sheet
int sheet_index = tile8_id / kTilesPerSheet;
// Clamp to valid sheet range (0-7)
return std::min(7, std::max(0, sheet_index));
}
// NEW: Get the actual palette slot for the current tile16 being edited
int Tile16Editor::GetActualPaletteSlotForCurrentTile16() const {
// For the current tile16, we need to determine which sheet the tile8s belong
// to and use the most appropriate palette region
if (current_tile8_ >= 0 &&
current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
int sheet_index = GetSheetIndexForTile8(current_tile8_);
return GetActualPaletteSlot(current_palette_, sheet_index);
}
// Default to sheet 0 (main blockset) if no tile8 selected
return GetActualPaletteSlot(current_palette_, 0);
}
int Tile16Editor::GetPaletteBaseForSheet(int sheet_index) const {
// Based on overworld palette structure and how ProcessGraphicsBuffer assigns
// colors: The 256-color palette is organized as 16 rows of 16 colors each.
// Different graphics sheets map to different palette regions:
//
// Row 0: Transparent/system colors
// Row 1: HUD colors (palette index 0x10-0x1F)
// Rows 2-4: MAIN/AUX1 palette region for main graphics
// Rows 5-7: AUX2 palette region for area-specific graphics
// Row 7: ANIMATED palette for animated tiles
//
// The palette_button (0-7) selects within the region.
switch (sheet_index) {
case 0: // Main blockset
case 3: // Area graphics set 1
case 4: // Area graphics set 2
return 2; // AUX1 palette region starts at row 2
case 5: // Area graphics set 3
case 6: // Area graphics set 4
return 5; // AUX2 palette region starts at row 5
case 1: // Main graphics
case 2: // Main graphics
return 2; // MAIN palette region starts at row 2
case 7: // Animated tiles
return 7; // ANIMATED palette region at row 7
default:
return 2; // Default to MAIN region
}
}
// Helper methods for palette management
absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
if (tile8_id < 0 ||
tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
return absl::InvalidArgumentError("Invalid tile8 ID");
}
if (!current_gfx_individual_[tile8_id].is_active()) {
return absl::OkStatus(); // Skip inactive tiles
}
if (!rom_) {
return absl::FailedPreconditionError("ROM not set");
}
// Use the complete 256-color overworld palette for consistency
gfx::SnesPalette display_palette;
if (overworld_palette_.size() >= 256) {
display_palette = overworld_palette_;
} else if (palette_.size() >= 256) {
display_palette = palette_;
} else {
// Fallback to ROM palette
const auto& palette_groups = rom()->palette_group();
if (palette_groups.overworld_main.size() > 0) {
display_palette = palette_groups.overworld_main[0];
} else {
return absl::FailedPreconditionError("No overworld palette available");
}
}
// Validate current_palette_ index
if (current_palette_ < 0 || current_palette_ >= 8) {
util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
current_palette_ = 0;
}
// // Use the same palette system as the overworld (complete 256-color
// palette) if (display_palette.size() >= 256) {
// // Apply complete 256-color palette (same as overworld system)
// // The pixel data already contains correct color indices for the
// 256-color palette
// current_gfx_individual_[tile8_id].SetPalette(display_palette);
// } else {
// For smaller palettes, use SetPaletteWithTransparent with current palette
current_gfx_individual_[tile8_id].SetPaletteWithTransparent(display_palette,
current_palette_);
// }
current_gfx_individual_[tile8_id].set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE,
&current_gfx_individual_[tile8_id]);
util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
tile8_id, current_palette_, display_palette.size());
return absl::OkStatus();
}
absl::Status Tile16Editor::RefreshAllPalettes() {
if (!rom_) {
return absl::FailedPreconditionError("ROM not set");
}
// Validate current_palette_ index
if (current_palette_ < 0 || current_palette_ >= 8) {
util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
current_palette_ = 0;
}
// CRITICAL FIX: Use the complete overworld palette for proper color
// coordination
gfx::SnesPalette display_palette;
if (overworld_palette_.size() >= 256) {
// Use the complete 256-color palette from overworld editor
display_palette = overworld_palette_;
util::logf("Using complete overworld palette with %zu colors",
display_palette.size());
} else if (palette_.size() >= 256) {
// Fallback to the old palette_ if it's complete
display_palette = palette_;
util::logf("Using fallback complete palette with %zu colors",
display_palette.size());
} else {
// Last resort: Use ROM palette groups
const auto& palette_groups = rom()->palette_group();
if (palette_groups.overworld_main.size() > 0) {
display_palette = palette_groups.overworld_main[0];
util::logf("Warning: Using ROM main palette with %zu colors",
display_palette.size());
} else {
return absl::FailedPreconditionError("No palette available");
}
}
// CRITICAL FIX: Use the same palette system as the overworld
// The overworld system applies the complete 256-color palette to the main
// graphics bitmap Individual tile8 graphics use the same palette but with
// proper color mapping
if (current_gfx_bmp_.is_active()) {
// Apply the complete 256-color palette to the source bitmap (same as
// overworld)
current_gfx_bmp_.SetPalette(display_palette);
current_gfx_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &current_gfx_bmp_);
util::logf(
"Applied complete 256-color palette to source bitmap (same as "
"overworld)");
}
// Update current tile16 being edited with complete 256-color palette
if (current_tile16_bmp_.is_active()) {
// Use complete 256-color palette (same as overworld system)
current_tile16_bmp_.SetPalette(display_palette);
current_tile16_bmp_.set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &current_tile16_bmp_);
}
// CRITICAL FIX: Update individual tile8 graphics with proper palette offsets
// Each tile8 belongs to a specific graphics sheet, which maps to a specific
// region of the 256-color palette. The current_palette_ (0-7) button selects
// within that region.
for (size_t i = 0; i < current_gfx_individual_.size(); ++i) {
if (current_gfx_individual_[i].is_active()) {
// Determine which sheet this tile belongs to and get the palette offset
int sheet_index = GetSheetIndexForTile8(static_cast<int>(i));
int palette_base = GetPaletteBaseForSheet(sheet_index);
// Calculate the palette offset in the 256-color palette:
// - palette_base * 16: row offset in the 16x16 palette grid
// - current_palette_: additional offset within the region (0-7 maps to
// different sub-palettes)
// For 4bpp SNES graphics, we use 8 colors per sub-palette with
// transparent index 0
size_t palette_offset = (palette_base * 16) + (current_palette_ * 8);
// Use SetPaletteWithTransparent to apply the correct 8-color sub-palette
// This extracts 7 colors starting at palette_offset and creates
// transparent index 0
current_gfx_individual_[i].SetPaletteWithTransparent(
display_palette, palette_offset, 7);
current_gfx_individual_[i].set_modified(true);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &current_gfx_individual_[i]);
}
}
util::logf(
"Successfully refreshed all palettes in tile16 editor using complete "
"256-color palette "
"(same as overworld system)");
return absl::OkStatus();
}
void Tile16Editor::DrawPaletteSettings() {
if (show_palette_settings_) {
if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
Text("Pixel Normalization & Color Correction:");
int mask_value = static_cast<int>(palette_normalization_mask_);
if (SliderInt("Normalization Mask", &mask_value, 1, 255, "0x%02X")) {
palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
}
Checkbox("Auto Normalize Pixels", &auto_normalize_pixels_);
if (Button("Apply to All Graphics")) {
auto reload_result = LoadTile8();
if (!reload_result.ok()) {
Text("Error: %s", reload_result.message().data());
}
}
SameLine();
if (Button("Reset Defaults")) {
palette_normalization_mask_ = 0x0F;
auto_normalize_pixels_ = true;
auto reload_result = LoadTile8();
(void)reload_result; // Suppress warning
}
Separator();
Text("Current State:");
static constexpr std::array<const char*, 7> palette_group_names = {
"OW Main", "OW Aux", "OW Anim", "Dungeon",
"Sprites", "Armor", "Sword"};
Text("Palette Group: %d (%s)", current_palette_group_,
(current_palette_group_ < 7)
? palette_group_names[current_palette_group_]
: "Unknown");
Text("Current Palette: %d", current_palette_);
Separator();
Text("Sheet-Specific Fixes:");
// Sheet-specific palette fixes
static bool fix_sheet_0 = true;
static bool fix_sprite_sheets = true;
static bool use_transparent_for_terrain = false;
if (Checkbox("Fix Sheet 0 (Trees)", &fix_sheet_0)) {
auto reload_result = LoadTile8();
if (!reload_result.ok()) {
Text("Error reloading: %s", reload_result.message().data());
}
}
HOVER_HINT(
"Use direct palette for sheet 0 instead of transparent palette");
if (Checkbox("Fix Sprite Sheets", &fix_sprite_sheets)) {
auto reload_result = LoadTile8();
if (!reload_result.ok()) {
Text("Error reloading: %s", reload_result.message().data());
}
}
HOVER_HINT("Use direct palette for sprite graphics sheets");
if (Checkbox("Transparent for Terrain", &use_transparent_for_terrain)) {
auto reload_result = LoadTile8();
if (!reload_result.ok()) {
Text("Error reloading: %s", reload_result.message().data());
}
}
HOVER_HINT("Force transparent palette for terrain graphics");
Separator();
Text("Color Analysis:");
if (current_tile8_ >= 0 &&
current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
current_gfx_individual_[current_tile8_].is_active()) {
Text("Selected Tile8 Analysis:");
const auto& tile_data =
current_gfx_individual_[current_tile8_].vector();
std::map<uint8_t, int> pixel_counts;
for (uint8_t pixel : tile_data) {
pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
}
Text("Pixel Value Distribution:");
for (const auto& pair : pixel_counts) {
int value = pair.first;
int count = pair.second;
Text(" Value %d (0x%X): %d pixels", value, value, count);
}
Text("Palette Colors Used:");
const auto& palette = current_gfx_individual_[current_tile8_].palette();
for (const auto& pair : pixel_counts) {
int value = pair.first;
int count = pair.second;
if (value < static_cast<int>(palette.size())) {
auto color = palette[value];
ImVec4 display_color = color.rgb();
ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
display_color, ImGuiColorEditFlags_NoTooltip,
ImVec2(16, 16));
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Index %d: 0x%04X (%d pixels)", value,
color.snes(), count);
}
if (value % 8 != 7)
ImGui::SameLine();
}
}
}
// Enhanced ROM Palette Management Section
Separator();
if (CollapsingHeader("ROM Palette Manager") && rom_) {
Text("Experimental ROM Palette Selection:");
HOVER_HINT(
"Use ROM palettes to experiment with different color schemes");
if (Button("Open Enhanced Palette Editor")) {
tile16_edit_canvas_.ShowPaletteEditor();
}
SameLine();
if (Button("Show Color Analysis")) {
tile16_edit_canvas_.ShowColorAnalysis();
}
// Quick palette application
static int quick_group = 0;
static int quick_index = 0;
SliderInt("ROM Group", &quick_group, 0, 6);
SliderInt("Palette Index", &quick_index, 0, 7);
if (Button("Apply to Tile8 Source")) {
if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
util::logf("Applied ROM palette group %d, index %d to Tile8 source",
quick_group, quick_index);
}
}
SameLine();
if (Button("Apply to Tile16 Editor")) {
if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
util::logf(
"Applied ROM palette group %d, index %d to Tile16 editor",
quick_group, quick_index);
}
}
}
}
End();
}
}
void Tile16Editor::DrawScratchSpace() {
Text("Layout Scratch:");
for (int i = 0; i < 4; i++) {
if (i > 0)
SameLine();
std::string slot_name = "S" + std::to_string(i + 1);
if (layout_scratch_[i].in_use) {
if (Button((slot_name + " Load").c_str(), ImVec2(40, 20))) {
// Load layout from scratch - placeholder for now
}
} else {
if (Button((slot_name + " Save").c_str(), ImVec2(40, 20))) {
// Save current layout to scratch - placeholder for now
}
}
}
}
absl::Status Tile16Editor::SaveLayoutToScratch(int slot) {
if (slot < 0 || slot >= 4) {
return absl::InvalidArgumentError("Invalid scratch slot");
}
// For now, just mark as used - full implementation would save current editing
// state
layout_scratch_[slot].in_use = true;
layout_scratch_[slot].name = absl::StrFormat("Layout %d", slot + 1);
return absl::OkStatus();
}
absl::Status Tile16Editor::LoadLayoutFromScratch(int slot) {
if (slot < 0 || slot >= 4) {
return absl::InvalidArgumentError("Invalid scratch slot");
}
if (!layout_scratch_[slot].in_use) {
return absl::FailedPreconditionError("Scratch slot is empty");
}
// Placeholder - full implementation would restore editing state
return absl::OkStatus();
}
void Tile16Editor::DrawManualTile8Inputs() {
if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("Manual Tile8 Configuration for Tile16 %02X", current_tile16_);
ImGui::Separator();
auto* tile_data = GetCurrentTile16Data();
if (tile_data) {
ImGui::Text("Current Tile16 ROM Data:");
// Display and edit each quadrant using TileInfo structure
const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
"Bottom-Right"};
for (int q = 0; q < 4; q++) {
ImGui::Text("%s Quadrant:", quadrant_names[q]);
// Get the current TileInfo for this quadrant
gfx::TileInfo* tile_info = nullptr;
switch (q) {
case 0:
tile_info = &tile_data->tile0_;
break;
case 1:
tile_info = &tile_data->tile1_;
break;
case 2:
tile_info = &tile_data->tile2_;
break;
case 3:
tile_info = &tile_data->tile3_;
break;
}
if (tile_info) {
// Editable inputs for TileInfo components
ImGui::PushID(q);
int tile_id_int = static_cast<int>(tile_info->id_);
if (ImGui::InputInt("Tile8 ID", &tile_id_int, 1, 10)) {
tile_info->id_ =
static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
}
int palette_int = static_cast<int>(tile_info->palette_);
if (ImGui::SliderInt("Palette", &palette_int, 0, 7)) {
tile_info->palette_ = static_cast<uint8_t>(palette_int);
}
ImGui::Checkbox("X Flip", &tile_info->horizontal_mirror_);
ImGui::SameLine();
ImGui::Checkbox("Y Flip", &tile_info->vertical_mirror_);
ImGui::SameLine();
ImGui::Checkbox("Priority", &tile_info->over_);
if (ImGui::Button("Apply to Graphics")) {
// Update the tiles_info array and regenerate graphics
tile_data->tiles_info[q] = *tile_info;
auto update_result = UpdateROMTile16Data();
if (!update_result.ok()) {
ImGui::Text("Error: %s", update_result.message().data());
}
auto refresh_result = SetCurrentTile(current_tile16_);
if (!refresh_result.ok()) {
ImGui::Text("Refresh Error: %s", refresh_result.message().data());
}
}
ImGui::PopID();
}
if (q < 3)
ImGui::Separator();
}
ImGui::Separator();
if (ImGui::Button("Apply All Changes")) {
auto update_result = UpdateROMTile16Data();
if (!update_result.ok()) {
ImGui::Text("Update Error: %s", update_result.message().data());
}
auto save_result = SaveTile16ToROM();
if (!save_result.ok()) {
ImGui::Text("Save Error: %s", save_result.message().data());
}
}
ImGui::SameLine();
if (ImGui::Button("Refresh Display")) {
auto refresh_result = SetCurrentTile(current_tile16_);
if (!refresh_result.ok()) {
ImGui::Text("Refresh Error: %s", refresh_result.message().data());
}
}
} else {
ImGui::Text("Tile16 data not accessible");
ImGui::Text("Current tile16: %d", current_tile16_);
if (rom_) {
ImGui::Text("Valid range: 0-4095 (4096 total tiles)");
}
}
ImGui::Separator();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
} // namespace editor
} // namespace yaze