7#include "absl/status/status.h"
8#include "absl/strings/str_format.h"
19#include "imgui/imgui.h"
29namespace fs = std::filesystem;
49 std::array<uint16_t, 4>
57 {16, 5, {0x2066, 0x6066, 0x2076, 0x6076}},
58 {16, 8, {0x2C88, 0x6C88, 0x2C89, 0x6C89}},
59 {16, 11, {0x3086, 0x7086, 0x3087, 0x7087}},
60 {16, 14, {0x3469, 0x7469, 0x3479, 0x7479}},
61 {16, 17, {0x30B4, 0x30B5, 0x30C4, 0x30C5}},
65 const std::array<gfx::SnesColor, 32>& colors) {
66 std::vector<std::array<uint8_t, 4>> out(32);
67 for (
int i = 0; i < 32; ++i) {
68 auto rgb = colors[i].rgb();
69 uint8_t alpha = (i % 4 == 0) ? 0 : 255;
70 out[i] = {
static_cast<uint8_t
>(rgb.x),
static_cast<uint8_t
>(rgb.y),
71 static_cast<uint8_t
>(rgb.z), alpha};
77 const std::array<gfx::SnesColor, 32>& colors) {
79 std::vector<gfx::SnesColor>(colors.begin(), colors.end()));
98 int width,
int height) {
100 if (width <= 0 || height <= 0)
103 SDL_Renderer* renderer =
nullptr;
104 SDL_Window* window = SDL_GetMouseFocus();
106 window = SDL_GetKeyboardFocus();
108 renderer = SDL_GetRenderer(window);
112 ref_texture_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
113 SDL_TEXTUREACCESS_STATIC, width, height);
116 SDL_SetTextureBlendMode(
ref_texture_, SDL_BLENDMODE_BLEND);
117 SDL_UpdateTexture(
ref_texture_,
nullptr, rgba.data(), width * 4);
134 "%s", tr(
"Open a .tilemap/.bin file, or pick one from the project "
135 "list above, to begin editing."));
137 ImGui::TextColored(theme.text_error_red,
"%s",
open_error_.c_str());
146 ImGui::TextColored(theme.status_warning,
"%s",
147 tr(
"This file changed on disk. Reload it? Your "
148 "unsaved edits will be lost if you do."));
149 if (ImGui::Button(tr(
"Reload"))) {
154 if (ImGui::Button(tr(
"Keep My Edits"))) {
166 if (ImGui::BeginTable(
"##MenuTilemapLayout", 2, ImGuiTableFlags_Resizable)) {
167 ImGui::TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch);
168 ImGui::TableSetupColumn(
"Tools", ImGuiTableColumnFlags_WidthFixed, 220);
169 ImGui::TableNextRow();
170 ImGui::TableSetColumnIndex(0);
175 ImGui::TableSetColumnIndex(1);
189 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) &&
190 (ImGui::GetIO().KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Super)) &&
191 ImGui::IsKeyPressed(ImGuiKey_S,
false)) {
197 if (ImGui::Button(tr(
"Open..."))) {
199 options.
filters.push_back({
"Menu tilemap",
"tilemap,bin"});
200 options.
filters.push_back({
"All files",
"*"});
207 if (project !=
nullptr) {
212 ImGui::SetNextItemWidth(280);
214 : std::string(tr(
"Project files..."));
215 if (ImGui::BeginCombo(
"##MenuTilemapProjectFiles", preview.c_str())) {
217 ImGui::TextDisabled(
"%s", tr(
"Masks & Rings (page 3)"));
222 std::string label = entry.label;
225 if (ImGui::Selectable(label.c_str(), is_current)) {
238 if (ImGui::Button(tr(
"Save")))
241 if (ImGui::Button(tr(
"Save As...")))
244 if (ImGui::Button(tr(
"Revert")))
255 if (project ==
nullptr || project->
filepath.empty())
258 fs::path root = fs::path(project->
filepath).parent_path();
261 auto add_dir = [&](
const fs::path& dir) {
263 if (!fs::exists(dir, ec))
265 for (
const auto& it : fs::directory_iterator(dir, ec)) {
268 if (!it.is_regular_file())
270 auto ext = it.path().extension().string();
271 if (ext !=
".tilemap" && ext !=
".bin")
275 entry.
label = fs::relative(it.path(), root, ec).string();
277 entry.
label = it.path().filename().string();
281 add_dir(root /
"Menu" /
"tilemaps");
282 add_dir(root /
"Menu" /
"rings");
286 return a.label < b.label;
293 return util::GetFileName(e.full_path) ==
309 doc_ = std::move(new_doc);
315 return absl::OkStatus();
320 return absl::FailedPreconditionError(
"nothing loaded");
326 return absl::FailedPreconditionError(
"nothing loaded");
331 return absl::CancelledError(
"save cancelled");
337 return absl::FailedPreconditionError(
"nothing loaded");
340 return absl::OkStatus();
348 const double now = ImGui::GetTime();
355 if (changed.ok() && *changed) {
362 bool changed =
false;
371 fs::path root = fs::path(project->
filepath).parent_path();
381 if (project !=
nullptr &&
388 fs::path(project->
filepath).parent_path().string(),
398 ImGui::TextUnformatted(tr(
"CHR:"));
401 if (ImGui::RadioButton(
"ROM##chr", chr_kind == 0)) {
406 if (ImGui::RadioButton(
"File##chr", chr_kind == 1)) {
412 if (ImGui::Button(
"...##chrfile")) {
414 options.
filters.push_back({
"2bpp CHR",
"bin,chr,4bpp,2bpp"});
427 ImGui::TextUnformatted(tr(
"Palette:"));
429 struct PaletteChoice {
433 static constexpr PaletteChoice kChoices[] = {
440 for (
const PaletteChoice& choice : kChoices) {
447 if (ImGui::Button(tr(
"Reload"))) {
450 if (ImGui::IsItemHovered()) {
451 ImGui::SetTooltip(
"%s",
452 tr(
"Re-read the CHR and palette sources (ROM symbol, "
453 "menu_palette.asm, files)."));
461 ImGui::SetNextItemWidth(180);
464 if (ImGui::InputText(
"##palLabel", buf,
sizeof(buf))) {
469 if (ImGui::Button(
"...##symfile")) {
471 options.
filters.push_back({
"Symbol file",
"sym,mlb"});
480 ImGui::TextDisabled(
"%s",
482 ?
"(no symbols loaded)"
486 ImGui::TextUnformatted(tr(
"menu_palette.asm:"));
488 if (ImGui::Button(
"...##asmfile")) {
490 options.
filters.push_back({
"ASM",
"asm"});
499 ImGui::TextDisabled(
"%s",
501 ?
"(none found in project)"
506 if (ImGui::Button(
"...##palfile")) {
508 options.
filters.push_back({
"Palette/CGRAM dump",
"pal,bin,cgram"});
516 ImGui::TextDisabled(
"%s",
544static std::string ShortenSourceError(
const absl::Status& status) {
545 std::string message(status.message());
546 if (message.find(
"every byte there is 0") != std::string::npos) {
547 return "symbol reads all zero (assembled data; base ROM)";
549 constexpr size_t kMax = 100;
550 if (message.size() > kMax) {
551 message.resize(kMax);
559 using PaletteColors = std::array<gfx::SnesColor, 32>;
561 auto from_symbol = [&]() -> absl::StatusOr<PaletteColors> {
562 if (rom ==
nullptr || !rom->
is_loaded()) {
563 return absl::FailedPreconditionError(
"no ROM loaded");
566 return absl::FailedPreconditionError(
567 "no symbol file loaded (the project's symbols_filename, or pick "
576 auto from_asm = [&]() -> absl::StatusOr<PaletteColors> {
578 return absl::NotFoundError(
579 "no Menu/menu_palette.asm found for this project (pick one with "
590 return result.status();
593 return absl::OkStatus();
596 if (game_data ==
nullptr) {
597 return absl::FailedPreconditionError(
"no GameData loaded");
601 return result.status();
603 *description =
"HUD palette (palette_groups.hud[0])";
604 return absl::OkStatus();
607 auto result = from_asm();
609 return result.status();
611 *description = absl::StrFormat(
"ASM %s (%s)", asm_name,
palette_label_);
612 return absl::OkStatus();
615 auto result = from_symbol();
617 return result.status();
620 return absl::OkStatus();
623 auto symbol = from_symbol();
626 *description = absl::StrFormat(
"ROM symbol %s (auto)",
palette_label_);
627 return absl::OkStatus();
629 auto asm_result = from_asm();
630 if (asm_result.ok()) {
632 *description = absl::StrFormat(
"ASM %s (auto: %s)", asm_name,
633 ShortenSourceError(symbol.status()));
634 return absl::OkStatus();
636 return absl::FailedPreconditionError(absl::StrFormat(
637 "symbol: %s; asm: %s", ShortenSourceError(symbol.status()),
638 ShortenSourceError(asm_result.status())));
641 return absl::InternalError(
"unknown palette source");
652 source_status_ = absl::StrFormat(
"CHR: %s", result.status().message());
653 return result.status();
657 if (rom ==
nullptr || !rom->
is_loaded()) {
663 source_status_ = absl::StrFormat(
"CHR: %s", result.status().message());
664 return result.status();
669 std::string palette_description;
670 absl::Status palette_status =
672 if (!palette_status.ok()) {
673 source_status_ = absl::StrFormat(
"Palette: %s", palette_status.message());
674 return palette_status;
681 return absl::OkStatus();
696 for (
const auto& icon : kPage3MaskIcons) {
697 for (
int sub = 0; sub < 4; ++sub) {
699 int cell_row = icon.row + sub / 2;
700 int cell_col = icon.col + sub % 2;
701 int base_x = cell_col * 8;
702 int base_y = cell_row * 8;
703 if (base_x + 8 > width || base_y + 8 > height)
705 for (
int y = 0; y < 8; ++y) {
707 for (
int x = 0; x < 8; ++x) {
709 uint8_t color = chr_fn(info.
id_, sx, sy) & 0x03;
711 static_cast<uint8_t
>((info.
palette_ & 0x07) * 4 + color);
712 size_t idx =
static_cast<size_t>(base_y + y) * width + (base_x + x);
714 indexed[idx] = index;
734 std::array<gfx::SnesColor, 32> slice{};
735 for (
int i = 0; i < 4; ++i) {
759 ImGui::SetNextItemWidth(120);
760 ImGui::SliderFloat(
"Zoom", &
zoom_, 1.0f, 4.0f,
"%.1fx");
763 if (ImGui::Button(tr(
"Copy"))) {
775 if (ImGui::Button(tr(
"Fill"))) {
785 if (ImGui::Button(tr(
"Erase"))) {
792 ImGui::SetNextItemWidth(90);
794 if (ImGui::InputInt(
"Erase word", &erase_val, 0, 0,
795 ImGuiInputTextFlags_CharsHexadecimal)) {
796 erase_word_ =
static_cast<uint16_t
>(std::clamp(erase_val, 0, 0xFFFF));
828 static_cast<uint8_t
>(std::clamp(
ref_opacity_, 0.0f, 1.0f) * 255.0f);
829 ImGui::GetWindowDrawList()->AddImage(
830 reinterpret_cast<ImTextureID
>(
ref_texture_), p_min, p_max, ImVec2(0, 0),
831 ImVec2(1, 1), ImGui::GetColorU32(IM_COL32(255, 255, 255, alpha)));
842 ImGui::GetIO().KeyAlt && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
843 bool right_click = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
844 if (
has_hover_ && (alt_click || right_click) &&
858 if (
has_hover_ && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
865 if (
stroke_active_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
869 if (
has_hover_ && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
883 if (
selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
894 ImGui::TextUnformatted(tr(
"Tile Picker"));
896 ImGui::TextDisabled(
"%s", tr(
"No CHR source loaded."));
913 const float available_width = ImGui::GetContentRegionAvail().x;
915 constexpr const char* kLabels[] = {
"Fit",
"1x",
"2x",
"4x"};
917 ImGui::AlignTextToFramePadding();
918 ImGui::TextDisabled(
"%s", tr(
"Scale"));
920 ImGui::SetNextItemWidth(80.0f);
921 if (ImGui::Combo(
"##MenuTilemapPickerScale", &mode_idx, kLabels,
922 IM_ARRAYSIZE(kLabels))) {
933 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
934 const bool grid_visible = ImGui::BeginChild(
935 "##MenuTilemapPickerGrid", ImVec2(0.0f, 220.0f), ImGuiChildFlags_None,
936 ImGuiWindowFlags_AlwaysVerticalScrollbar);
937 ImGui::PopStyleVar();
941 if ((result.tile_clicked || result.selection_changed) &&
942 result.selected_tile >= 0) {
949 ImGui::SetNextItemWidth(140);
951 if (ImGui::IsItemDeactivatedAfterEdit())
953 ImGui::Checkbox(tr(
"H Flip"), &
h_flip_);
955 ImGui::Checkbox(tr(
"V Flip"), &
v_flip_);
956 ImGui::Checkbox(tr(
"Priority"), &
priority_);
964 ImGui::Text(
"x=%d y=%d tile=%03X pal=%d H=%d V=%d P=%d word=%04X",
967 info.
over_ ? 1 : 0, word);
971 if (ImGui::CollapsingHeader(tr(
"Preview Overlays"))) {
975 if (ImGui::Button(
"...##refimg")) {
977 options.
filters.push_back({
"PNG",
"png"});
986 std::vector<uint8_t> rgba(png->indices.size() * 4);
987 for (
size_t i = 0; i < png->indices.size(); ++i) {
988 auto& c = png->palette[png->indices[i]];
989 rgba[i * 4 + 0] = c[0];
990 rgba[i * 4 + 1] = c[1];
991 rgba[i * 4 + 2] = c[2];
992 rgba[i * 4 + 3] = c[3];
1002 ImGui::SliderFloat(tr(
"Opacity"), &
ref_opacity_, 0.0f, 1.0f);
1003 ImGui::SliderFloat(
"dx", &
ref_nudge_.x, -32.0f, 32.0f);
1005 ImGui::SliderFloat(
"dy", &
ref_nudge_.y, -32.0f, 32.0f);
1008 ImGui::Checkbox(tr(
"WRAM composite dump ($7E1000-17FF)"),
1012 if (ImGui::Button(
"...##wramdump")) {
1014 options.
filters.push_back({
"WRAM dump",
"bin"});
1016 if (!path.empty()) {
1033 if (ImGui::Checkbox(tr(
"Dynamic content (page 3 mask icons, read-only)"),
1038 ImGui::TextDisabled(
1039 "%s", tr(
"Display only -- the five mask icons at their fixed "
1040 "menu_offset() cells. Not saved into this file, and not the "
1041 "title text or ring slots (see docs)."));
1047 ImGui::Text(
"%s",
doc_.
dirty() ? tr(
"Modified") : tr(
"Saved"));
1086 const std::vector<uint8_t>& bytes) {
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Manages undo/redo stacks for a single editor.
void Push(std::unique_ptr< UndoAction > action)
Provider for symbol (label) resolution in disassembly.
absl::Status LoadSymbolFile(const std::string &path, SymbolFormat format=SymbolFormat::kAuto)
Load symbols from a .sym file (various formats)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
SNES 16-bit tile metadata container.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
auto hover_mouse_pos() const
bool DrawTileSelector(int size, int size_y=0)
void SetCanvasSize(ImVec2 canvas_size)
bool IsMouseHovering() const
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
void SetGlobalScale(float scale)
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
RAII guard for ImGui style colors.
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
const AgentUITheme & GetTheme()
TileInfo WordToTileInfo(uint16_t word)
AdaptiveSheetLayout ResolveAdaptiveSheetLayout(float available_width, int source_width, int source_height, AdaptiveSheetScaleMode mode=AdaptiveSheetScaleMode::kFit, float min_scale=0.35f, float max_scale=4.0f, float horizontal_chrome=24.0f)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFile(const std::string &path)
std::string GetFileName(const std::string &filename)
Gets the filename from a full path.
absl::StatusOr< PngImage > DecodePng(const std::vector< uint8_t > &bytes)
std::string GetFileExtension(const std::string &filename)
Gets the file extension from a filename.
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromHud(const GameData &game_data)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromFile(const std::string &path, size_t byte_offset)
absl::StatusOr< std::vector< uint8_t > > ResolveMenuChrFromRom(const Rom &rom)
std::optional< std::string > FindMenuPaletteAsm(const std::string &project_dir, const std::string &code_folder)
absl::StatusOr< std::vector< uint8_t > > ResolveMenuChrFromFile(const std::string &path)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromAsm(const std::string &path, const std::string &label)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromSymbol(const Rom &rom, const emu::debug::SymbolProvider &symbols, const std::string &label)
MenuTilemapDocument::ChrPixelFn MakeChrPixelFn(const std::vector< uint8_t > &sheet_8bpp_indexed)
#define RETURN_IF_ERROR(expr)
Unified screen editor snapshot.
MenuTilemapSnapshot menu_tilemap
Modern project structure with comprehensive settings consolidation.
std::string symbols_filename
std::vector< FileDialogFilter > filters