yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
screen_menu_tilemap_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdio>
5#include <filesystem>
6
7#include "absl/status/status.h"
8#include "absl/strings/str_format.h"
16#include "app/gui/core/color.h"
18#include "core/project.h"
19#include "imgui/imgui.h"
20#include "util/file_util.h"
21#include "util/i18n/tr.h"
22#include "util/indexed_png.h"
23#include "util/macro.h"
25
26namespace yaze {
27namespace editor {
28
29namespace fs = std::filesystem;
30
31namespace {
32
33// menu_offset(row,col) = row*64 + col*2 -- see Core/symbols.asm in Oracle
34// of Secrets. The five mask icons on the "Masks & Rings" page 3 prototype
35// (Menu/menu_page3.asm, Menu_Page3_Draw) are each a 2x2 block of tiles
36// written directly into the $1000 buffer at these (row, col) anchors, from
37// the 4-word *GFX tables in Menu/menu_gfx_table.asm. These are *not*
38// stored in ring_box.tilemap itself -- the frame file only has the empty
39// background under them -- so this is an optional read-only overlay, not
40// something this editor can paint into the tilemap file. The title text
41// ("MASKS RINGS" at menu_offset(6,10)) and the six ring slots
42// (Menu_DrawMagicRingsInBox, ownership-dependent) are *not* included here:
43// the title needs the menu's font/text encoding table and the ring slots
44// depend on which rings are owned, both well beyond "a few regexes" to
45// derive safely, per the brief.
47 int row;
48 int col;
49 std::array<uint16_t, 4>
50 words; // top-left, top-right, bottom-left, bottom-right
51};
52// Values transcribed from Menu/menu_gfx_table.asm (DekuMaskGFX,
53// ZoraMaskGFX, WolfMaskGFX, BunnyHoodGFX, StoneMaskGFX) and the anchor
54// (row, col) + GFX-table pairing from Menu_Page3_Draw in
55// Menu/menu_page3.asm.
57 {16, 5, {0x2066, 0x6066, 0x2076, 0x6076}}, // DekuMaskGFX
58 {16, 8, {0x2C88, 0x6C88, 0x2C89, 0x6C89}}, // ZoraMaskGFX
59 {16, 11, {0x3086, 0x7086, 0x3087, 0x7087}}, // WolfMaskGFX
60 {16, 14, {0x3469, 0x7469, 0x3479, 0x7479}}, // BunnyHoodGFX
61 {16, 17, {0x30B4, 0x30B5, 0x30C4, 0x30C5}}, // StoneMaskGFX
62};
63
64std::vector<std::array<uint8_t, 4>> BuildRgbaPalette(
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};
72 }
73 return out;
74}
75
77 const std::array<gfx::SnesColor, 32>& colors) {
78 return gfx::SnesPalette(
79 std::vector<gfx::SnesColor>(colors.begin(), colors.end()));
80}
81
82} // namespace
83
87
89 if (ref_texture_) {
90 SDL_DestroyTexture(ref_texture_);
91 ref_texture_ = nullptr;
94 }
95}
96
97void MenuTilemapEditorUI::SetReferenceImage(const std::vector<uint8_t>& rgba,
98 int width, int height) {
100 if (width <= 0 || height <= 0)
101 return;
102
103 SDL_Renderer* renderer = nullptr;
104 SDL_Window* window = SDL_GetMouseFocus();
105 if (!window)
106 window = SDL_GetKeyboardFocus();
107 if (window)
108 renderer = SDL_GetRenderer(window);
109 if (!renderer)
110 return;
111
112 ref_texture_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
113 SDL_TEXTUREACCESS_STATIC, width, height);
114 if (!ref_texture_)
115 return;
116 SDL_SetTextureBlendMode(ref_texture_, SDL_BLENDMODE_BLEND);
117 SDL_UpdateTexture(ref_texture_, nullptr, rgba.data(), width * 4);
118 ref_texture_width_ = width;
119 ref_texture_height_ = height;
120 ref_loaded_ = true;
121}
122
124 project::YazeProject* project,
125 UndoManager* undo_manager) {
126 const auto& theme = AgentUI::GetTheme();
127 undo_manager_ = undo_manager;
128
129 DrawFileBar(rom, project);
130 ImGui::Separator();
131
132 if (!loaded_) {
133 ImGui::TextWrapped(
134 "%s", tr("Open a .tilemap/.bin file, or pick one from the project "
135 "list above, to begin editing."));
136 if (!open_error_.empty()) {
137 ImGui::TextColored(theme.text_error_red, "%s", open_error_.c_str());
138 }
139 return;
140 }
141
142 DrawSourceBar(rom, game_data, project);
143 ImGui::Separator();
144
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"))) {
151 show_reload_prompt_ = false;
152 }
153 ImGui::SameLine();
154 if (ImGui::Button(tr("Keep My Edits"))) {
155 // Adopt the on-disk version as the baseline; otherwise the very next
156 // poll would report the same external change and re-open this prompt.
158 show_reload_prompt_ = false;
159 }
160 ImGui::Separator();
161 }
162
163 DrawToolbar();
164 ImGui::Separator();
165
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);
174
175 ImGui::TableSetColumnIndex(1);
177
178 ImGui::EndTable();
179 }
180
181 ImGui::Separator();
183
184 // ShortcutManager gives the Screen editor its own Ctrl/Cmd+S while
185 // that editor is active (Scope::kEditor keyed to EditorType::kScreen --
186 // see shortcut_configurator.cc); this panel-local accelerator only fires
187 // while the mouse is over this panel's content, so it never competes
188 // with either the global save or the editor-wide one.
189 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) &&
190 (ImGui::GetIO().KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Super)) &&
191 ImGui::IsKeyPressed(ImGuiKey_S, false)) {
192 SaveCurrent();
193 }
194}
195
197 if (ImGui::Button(tr("Open..."))) {
199 options.filters.push_back({"Menu tilemap", "tilemap,bin"});
200 options.filters.push_back({"All files", "*"});
201 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
202 if (!path.empty()) {
203 LoadFile(path);
204 }
205 }
206
207 if (project != nullptr) {
209 RefreshProjectFileList(project);
210 }
211 ImGui::SameLine();
212 ImGui::SetNextItemWidth(280);
213 std::string preview = loaded_ ? util::GetFileName(doc_.path())
214 : std::string(tr("Project files..."));
215 if (ImGui::BeginCombo("##MenuTilemapProjectFiles", preview.c_str())) {
216 if (project_has_page3_) {
217 ImGui::TextDisabled("%s", tr("Masks & Rings (page 3)"));
218 ImGui::Separator();
219 }
220 for (const auto& entry : project_files_) {
221 bool is_current = loaded_ && entry.full_path == doc_.path();
222 std::string label = entry.label;
223 if (is_current && doc_.dirty())
224 label += " *";
225 if (ImGui::Selectable(label.c_str(), is_current)) {
226 LoadFile(entry.full_path);
227 }
228 }
229 ImGui::EndCombo();
230 }
231 }
232
233 if (loaded_) {
234 ImGui::SameLine();
235 ImGui::Text("%s%s", util::GetFileName(doc_.path()).c_str(),
236 doc_.dirty() ? " *" : "");
237 ImGui::SameLine();
238 if (ImGui::Button(tr("Save")))
239 SaveCurrent();
240 ImGui::SameLine();
241 if (ImGui::Button(tr("Save As...")))
243 ImGui::SameLine();
244 if (ImGui::Button(tr("Revert")))
246
248 }
249}
250
252 project::YazeProject* project) {
253 project_files_.clear();
254 project_files_stale_ = false;
255 if (project == nullptr || project->filepath.empty())
256 return;
257
258 fs::path root = fs::path(project->filepath).parent_path();
259 project_has_page3_ = fs::exists(root / "Menu" / "menu_page3.asm");
260
261 auto add_dir = [&](const fs::path& dir) {
262 std::error_code ec;
263 if (!fs::exists(dir, ec))
264 return;
265 for (const auto& it : fs::directory_iterator(dir, ec)) {
266 if (ec)
267 break;
268 if (!it.is_regular_file())
269 continue;
270 auto ext = it.path().extension().string();
271 if (ext != ".tilemap" && ext != ".bin")
272 continue;
273 FileListEntry entry;
274 entry.full_path = it.path().string();
275 entry.label = fs::relative(it.path(), root, ec).string();
276 if (ec)
277 entry.label = it.path().filename().string();
278 project_files_.push_back(std::move(entry));
279 }
280 };
281 add_dir(root / "Menu" / "tilemaps");
282 add_dir(root / "Menu" / "rings");
283
284 std::sort(project_files_.begin(), project_files_.end(),
285 [](const FileListEntry& a, const FileListEntry& b) {
286 return a.label < b.label;
287 });
288 if (project_has_page3_) {
289 // ring_box.tilemap drives the "Masks & Rings" page 3 prototype
290 // (Menu_Page3_Draw / Menu_DrawRingBox) -- surface it first.
291 auto it = std::find_if(project_files_.begin(), project_files_.end(),
292 [](const FileListEntry& e) {
293 return util::GetFileName(e.full_path) ==
294 "ring_box.tilemap";
295 });
296 if (it != project_files_.end() && it != project_files_.begin()) {
297 std::rotate(project_files_.begin(), it, it + 1);
298 }
299 }
300}
301
302absl::Status MenuTilemapEditorUI::LoadFile(const std::string& path) {
304 absl::Status status = new_doc.LoadFromFile(path);
305 if (!status.ok()) {
306 open_error_ = std::string(status.message());
307 return status;
308 }
309 doc_ = std::move(new_doc);
310 loaded_ = true;
311 open_error_.clear();
312 show_reload_prompt_ = false;
313 has_selection_ = false;
315 return absl::OkStatus();
316}
317
319 if (!loaded_)
320 return absl::FailedPreconditionError("nothing loaded");
321 return doc_.Save();
322}
323
325 if (!loaded_)
326 return absl::FailedPreconditionError("nothing loaded");
327 std::string default_name = util::GetFileName(doc_.path());
329 default_name, util::GetFileExtension(doc_.path()));
330 if (path.empty())
331 return absl::CancelledError("save cancelled");
332 return doc_.SaveAs(path);
333}
334
336 if (!loaded_)
337 return absl::FailedPreconditionError("nothing loaded");
340 return absl::OkStatus();
341}
342
345 return;
346 // Detection re-reads the (<= 2 KB) file, so poll a couple of times a
347 // second rather than every frame.
348 const double now = ImGui::GetTime();
349 if (last_external_check_time_ >= 0.0 &&
350 now - last_external_check_time_ < 0.5) {
351 return;
352 }
354 auto changed = doc_.ExternalChangeDetected();
355 if (changed.ok() && *changed) {
356 show_reload_prompt_ = true;
357 }
358}
359
361 project::YazeProject* project) {
362 bool changed = false;
363
364 // Auto-fill the symbol file from the project the first time one is
365 // available, so the common case (an Oracle-style project with
366 // symbols_filename set) needs no manual picking. A user-picked path
367 // (symbols_path_from_project_ == false after an explicit "..." pick)
368 // is never clobbered by this.
369 if (project != nullptr && !project->symbols_filename.empty() &&
371 fs::path root = fs::path(project->filepath).parent_path();
372 std::string resolved = (root / project->symbols_filename).string();
373 if (resolved != last_symbols_path_) {
374 last_symbols_path_ = resolved;
376 changed = true;
377 }
378 }
379 // Same idea for Menu/menu_palette.asm: find it under the project once per
380 // project/code-folder (not every frame), never clobbering a picked file.
381 if (project != nullptr &&
383 const std::string key = project->filepath + "|" + project->code_folder;
384 if (key != asm_search_key_) {
385 asm_search_key_ = key;
386 std::string found =
388 fs::path(project->filepath).parent_path().string(),
389 project->code_folder)
390 .value_or("");
391 if (found != palette_asm_path_) {
392 palette_asm_path_ = found;
394 changed = true;
395 }
396 }
397 }
398 ImGui::TextUnformatted(tr("CHR:"));
399 ImGui::SameLine();
400 int chr_kind = static_cast<int>(chr_source_kind_);
401 if (ImGui::RadioButton("ROM##chr", chr_kind == 0)) {
403 changed = true;
404 }
405 ImGui::SameLine();
406 if (ImGui::RadioButton("File##chr", chr_kind == 1)) {
408 changed = true;
409 }
411 ImGui::SameLine();
412 if (ImGui::Button("...##chrfile")) {
414 options.filters.push_back({"2bpp CHR", "bin,chr,4bpp,2bpp"});
415 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
416 if (!path.empty()) {
417 chr_file_path_ = path;
418 changed = true;
419 }
420 }
421 ImGui::SameLine();
422 ImGui::TextDisabled("%s", chr_file_path_.empty()
423 ? "(none)"
425 }
426
427 ImGui::TextUnformatted(tr("Palette:"));
428 ImGui::SameLine();
429 struct PaletteChoice {
430 const char* label;
432 };
433 static constexpr PaletteChoice kChoices[] = {
434 {"Auto##pal", PaletteSourceKind::kAuto},
435 {"Symbol##pal", PaletteSourceKind::kSymbol},
436 {"ASM##pal", PaletteSourceKind::kAsm},
437 {"HUD##pal", PaletteSourceKind::kHud},
438 {"File##pal", PaletteSourceKind::kFile},
439 };
440 for (const PaletteChoice& choice : kChoices) {
441 if (ImGui::RadioButton(choice.label, palette_source_kind_ == choice.kind)) {
442 palette_source_kind_ = choice.kind;
443 changed = true;
444 }
445 ImGui::SameLine();
446 }
447 if (ImGui::Button(tr("Reload"))) {
448 changed = true; // e.g. after editing menu_palette.asm on disk
449 }
450 if (ImGui::IsItemHovered()) {
451 ImGui::SetTooltip("%s",
452 tr("Re-read the CHR and palette sources (ROM symbol, "
453 "menu_palette.asm, files)."));
454 }
455
456 const bool uses_symbol = palette_source_kind_ == PaletteSourceKind::kAuto ||
458 const bool uses_asm = palette_source_kind_ == PaletteSourceKind::kAuto ||
460 if (uses_symbol) {
461 ImGui::SetNextItemWidth(180);
462 char buf[128];
463 std::snprintf(buf, sizeof(buf), "%s", palette_label_.c_str());
464 if (ImGui::InputText("##palLabel", buf, sizeof(buf))) {
465 palette_label_ = buf;
466 changed = true;
467 }
468 ImGui::SameLine();
469 if (ImGui::Button("...##symfile")) {
471 options.filters.push_back({"Symbol file", "sym,mlb"});
472 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
473 if (!path.empty()) {
474 last_symbols_path_ = path;
476 changed = true;
477 }
478 }
479 ImGui::SameLine();
480 ImGui::TextDisabled("%s",
481 last_symbols_path_.empty()
482 ? "(no symbols loaded)"
484 }
485 if (uses_asm) {
486 ImGui::TextUnformatted(tr("menu_palette.asm:"));
487 ImGui::SameLine();
488 if (ImGui::Button("...##asmfile")) {
490 options.filters.push_back({"ASM", "asm"});
491 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
492 if (!path.empty()) {
493 palette_asm_path_ = path;
495 changed = true;
496 }
497 }
498 ImGui::SameLine();
499 ImGui::TextDisabled("%s",
500 palette_asm_path_.empty()
501 ? "(none found in project)"
503 }
505 ImGui::SameLine();
506 if (ImGui::Button("...##palfile")) {
508 options.filters.push_back({"Palette/CGRAM dump", "pal,bin,cgram"});
509 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
510 if (!path.empty()) {
511 palette_file_path_ = path;
512 changed = true;
513 }
514 }
515 ImGui::SameLine();
516 ImGui::TextDisabled("%s",
517 palette_file_path_.empty()
518 ? "(none)"
520 }
521
522 // Resolve once per change (kind, path, label, ROM), not every frame --
523 // including when resolution fails, so an error doesn't re-decompress the
524 // CHR sheets and re-read files on every draw.
525 if (changed || rom != resolved_rom_) {
526 sources_dirty_ = true;
527 }
528 if (sources_dirty_) {
529 resolved_rom_ = rom;
530 ResolveSources(rom, game_data);
531 }
532
533 const auto& theme = AgentUI::GetTheme();
534 if (!source_status_.empty()) {
535 gui::StyleColorGuard status_color(
536 ImGuiCol_Text,
537 sources_ready_ ? theme.status_success : theme.text_error_red);
538 ImGui::TextWrapped("%s", source_status_.c_str());
539 }
540}
541
542// Short, single-line reason for an Auto fallback (the all-zero symbol
543// message is a paragraph; the panel only needs the gist).
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)";
548 }
549 constexpr size_t kMax = 100;
550 if (message.size() > kMax) {
551 message.resize(kMax);
552 message += "...";
553 }
554 return message;
555}
556
558 Rom* rom, zelda3::GameData* game_data, std::string* description) {
559 using PaletteColors = std::array<gfx::SnesColor, 32>;
560
561 auto from_symbol = [&]() -> absl::StatusOr<PaletteColors> {
562 if (rom == nullptr || !rom->is_loaded()) {
563 return absl::FailedPreconditionError("no ROM loaded");
564 }
565 if (last_symbols_path_.empty()) {
566 return absl::FailedPreconditionError(
567 "no symbol file loaded (the project's symbols_filename, or pick "
568 "one with ...)");
569 }
571 absl::Status loaded = symbols.LoadSymbolFile(last_symbols_path_);
572 if (!loaded.ok())
573 return loaded;
575 };
576 auto from_asm = [&]() -> absl::StatusOr<PaletteColors> {
577 if (palette_asm_path_.empty()) {
578 return absl::NotFoundError(
579 "no Menu/menu_palette.asm found for this project (pick one with "
580 "...)");
581 }
583 };
584 const std::string asm_name = util::GetFileName(palette_asm_path_);
585
586 switch (palette_source_kind_) {
589 if (!result.ok())
590 return result.status();
591 palette_colors_ = *result;
592 *description = "file " + util::GetFileName(palette_file_path_);
593 return absl::OkStatus();
594 }
596 if (game_data == nullptr) {
597 return absl::FailedPreconditionError("no GameData loaded");
598 }
599 auto result = zelda3::ResolveMenuPaletteFromHud(*game_data);
600 if (!result.ok())
601 return result.status();
602 palette_colors_ = *result;
603 *description = "HUD palette (palette_groups.hud[0])";
604 return absl::OkStatus();
605 }
607 auto result = from_asm();
608 if (!result.ok())
609 return result.status();
610 palette_colors_ = *result;
611 *description = absl::StrFormat("ASM %s (%s)", asm_name, palette_label_);
612 return absl::OkStatus();
613 }
615 auto result = from_symbol();
616 if (!result.ok())
617 return result.status();
618 palette_colors_ = *result;
619 *description = absl::StrFormat("ROM symbol %s", palette_label_);
620 return absl::OkStatus();
621 }
623 auto symbol = from_symbol();
624 if (symbol.ok()) {
625 palette_colors_ = *symbol;
626 *description = absl::StrFormat("ROM symbol %s (auto)", palette_label_);
627 return absl::OkStatus();
628 }
629 auto asm_result = from_asm();
630 if (asm_result.ok()) {
631 palette_colors_ = *asm_result;
632 *description = absl::StrFormat("ASM %s (auto: %s)", asm_name,
633 ShortenSourceError(symbol.status()));
634 return absl::OkStatus();
635 }
636 return absl::FailedPreconditionError(absl::StrFormat(
637 "symbol: %s; asm: %s", ShortenSourceError(symbol.status()),
638 ShortenSourceError(asm_result.status())));
639 }
640 }
641 return absl::InternalError("unknown palette source");
642}
643
645 zelda3::GameData* game_data) {
646 sources_ready_ = false;
647 sources_dirty_ = false;
648
651 if (!result.ok()) {
652 source_status_ = absl::StrFormat("CHR: %s", result.status().message());
653 return result.status();
654 }
655 chr_sheet_ = std::move(*result);
656 } else {
657 if (rom == nullptr || !rom->is_loaded()) {
658 source_status_ = "CHR: no ROM loaded";
659 return absl::FailedPreconditionError(source_status_);
660 }
661 auto result = zelda3::ResolveMenuChrFromRom(*rom);
662 if (!result.ok()) {
663 source_status_ = absl::StrFormat("CHR: %s", result.status().message());
664 return result.status();
665 }
666 chr_sheet_ = std::move(*result);
667 }
668
669 std::string palette_description;
670 absl::Status palette_status =
671 ResolvePaletteSource(rom, game_data, &palette_description);
672 if (!palette_status.ok()) {
673 source_status_ = absl::StrFormat("Palette: %s", palette_status.message());
674 return palette_status;
675 }
676
677 sources_ready_ = true;
678 source_status_ = absl::StrFormat("CHR: %zu tiles | Palette: %s",
679 chr_sheet_.size() / 64, palette_description);
681 return absl::OkStatus();
682}
683
685 if (!loaded_)
686 return;
687 auto chr_fn = zelda3::MakeChrPixelFn(chr_sheet_);
688 std::vector<uint8_t> indexed = doc_.RenderIndexed(chr_fn);
689
691 // Display-only overlay: paints the page-3 mask icons directly into the
692 // rendered pixels (never into doc_'s bytes -- these tiles are written
693 // by ASM at runtime, not stored in ring_box.tilemap itself).
694 int width = doc_.render_width();
695 int height = doc_.render_height();
696 for (const auto& icon : kPage3MaskIcons) {
697 for (int sub = 0; sub < 4; ++sub) {
698 gfx::TileInfo info = gfx::WordToTileInfo(icon.words[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)
704 continue;
705 for (int y = 0; y < 8; ++y) {
706 int sy = info.vertical_mirror_ ? 7 - y : y;
707 for (int x = 0; x < 8; ++x) {
708 int sx = info.horizontal_mirror_ ? 7 - x : x;
709 uint8_t color = chr_fn(info.id_, sx, sy) & 0x03;
710 uint8_t index =
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);
713 if (color != 0)
714 indexed[idx] = index; // color 0 = transparent
715 }
716 }
717 }
718 }
719 }
720
722 canvas_bitmap_.SetPalette(BuildSnesPalette(palette_colors_));
727
728 if (!chr_sheet_.empty()) {
729 picker_width_ = 128;
730 picker_height_ = static_cast<int>(chr_sheet_.size() / picker_width_);
732 chr_sheet_; // raw 2bpp values 0-3; palette below maps them
734 std::array<gfx::SnesColor, 32> slice{};
735 for (int i = 0; i < 4; ++i) {
736 slice[i] = palette_colors_[selected_palette_ * 4 + i];
737 }
738 picker_bitmap_.SetPalette(BuildSnesPalette(slice));
743 }
744}
745
747 if (ImGui::RadioButton(tr("Paint"), tool_ == Tool::kPaint)) {
749 }
750 ImGui::SameLine();
751 if (ImGui::RadioButton(tr("Select"), tool_ == Tool::kSelect)) {
753 }
754 ImGui::SameLine();
755 ImGui::Checkbox(tr("Grid"), &show_grid_);
756 ImGui::SameLine();
757 ImGui::Checkbox(tr("Tint Priority"), &show_priority_tint_);
758 ImGui::SameLine();
759 ImGui::SetNextItemWidth(120);
760 ImGui::SliderFloat("Zoom", &zoom_, 1.0f, 4.0f, "%.1fx");
761
763 if (ImGui::Button(tr("Copy"))) {
765 has_clipboard_ = true;
766 }
767 ImGui::SameLine();
768 if (ImGui::Button(tr("Paste")) && has_clipboard_) {
769 BeginStroke("Paste");
771 CommitStroke();
773 }
774 ImGui::SameLine();
775 if (ImGui::Button(tr("Fill"))) {
776 BeginStroke("Fill selection");
777 gfx::TileInfo info(static_cast<uint16_t>(selected_tile_id_),
778 static_cast<uint8_t>(selected_palette_), v_flip_,
780 doc_.FillRect(selection_, info);
781 CommitStroke();
783 }
784 ImGui::SameLine();
785 if (ImGui::Button(tr("Erase"))) {
786 BeginStroke("Erase selection");
788 CommitStroke();
790 }
791 ImGui::SameLine();
792 ImGui::SetNextItemWidth(90);
793 int erase_val = erase_word_;
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));
797 }
798 }
799}
800
802 // Keep the canvas's own scale (used internally for grid lines and for
803 // converting screen-space mouse position back to unscaled tile-pixel
804 // coordinates in points()/hover_mouse_pos()) in lockstep with the zoom
805 // slider passed to DrawBitmap() below -- otherwise the grid/hover/click
806 // mapping and the visible bitmap drift apart as zoom_ changes.
809 ImVec2(doc_.render_width() * zoom_, doc_.render_height() * zoom_));
810 last_canvas_screen_origin_ = ImGui::GetCursorScreenPos();
813
816 }
817
820 }
821
822 if (show_ref_overlay_ && ref_loaded_ && ref_texture_ != nullptr) {
823 ImVec2 p_min(last_canvas_screen_origin_.x + ref_nudge_.x * zoom_,
825 ImVec2 p_max(p_min.x + ref_texture_width_ * zoom_,
826 p_min.y + ref_texture_height_ * zoom_);
827 uint8_t alpha =
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)));
832 }
833
835 if (has_hover_) {
836 ImVec2 mp = canvas_.hover_mouse_pos();
837 hover_col_ = static_cast<int>(mp.x) / 8;
838 hover_row_ = static_cast<int>(mp.y) / 8;
839 }
840
841 bool alt_click =
842 ImGui::GetIO().KeyAlt && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
843 bool right_click = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
844 if (has_hover_ && (alt_click || right_click) &&
847 }
848
849 if (tool_ == Tool::kPaint) {
850 // Canvas::DrawTileSelector() only reports a *double*-click via its
851 // return value (see canvas_runtime_draw.cc) -- it still updates hover
852 // bookkeeping on a single click, which we don't need since has_hover_/
853 // hover_row_/hover_col_ (above) already give us the same thing. Paint
854 // directly off those: single click paints one cell, holding the button
855 // down paints continuously (click-drag), matching a normal tile
856 // editor rather than requiring a double-click per cell.
858 if (has_hover_ && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
860 if (!stroke_active_)
861 BeginStroke("Paint tile");
864 }
865 if (stroke_active_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
866 CommitStroke();
867 }
868 } else if (tool_ == Tool::kSelect) {
869 if (has_hover_ && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
870 selecting_ = true;
871 select_drag_start_ = ImVec2(static_cast<float>(hover_col_),
872 static_cast<float>(hover_row_));
873 }
874 if (selecting_ && has_hover_) {
875 int c0 = static_cast<int>(select_drag_start_.x);
876 int r0 = static_cast<int>(select_drag_start_.y);
877 selection_.col = std::min(c0, hover_col_);
878 selection_.row = std::min(r0, hover_row_);
879 selection_.cols = std::abs(hover_col_ - c0) + 1;
880 selection_.rows = std::abs(hover_row_ - r0) + 1;
881 has_selection_ = true;
882 }
883 if (selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
884 selecting_ = false;
885 }
886 }
887
888 if (show_grid_)
889 canvas_.DrawGrid(8.0f);
891}
892
894 ImGui::TextUnformatted(tr("Tile Picker"));
895 if (chr_sheet_.empty() || picker_width_ <= 0 || picker_height_ <= 0) {
896 ImGui::TextDisabled("%s", tr("No CHR source loaded."));
897 return;
898 }
899
900 // Adaptive sheet layout (Fit/1x/2x/4x): the CHR sheet can be a few
901 // hundred pixels tall (7 sheets * 64px from Load2BppGraphics), so this
902 // matches the room-graphics / overworld tile16-selector convention
903 // rather than always showing it at native size.
904 const int tiles_per_row = picker_width_ / 8;
905 const int total_tiles = tiles_per_row * (picker_height_ / 8);
907 tile_picker_widget_.SetTilesPerRow(tiles_per_row);
911 }
912
913 const float available_width = ImGui::GetContentRegionAvail().x;
914 {
915 constexpr const char* kLabels[] = {"Fit", "1x", "2x", "4x"};
916 int mode_idx = static_cast<int>(picker_scale_mode_);
917 ImGui::AlignTextToFramePadding();
918 ImGui::TextDisabled("%s", tr("Scale"));
919 ImGui::SameLine();
920 ImGui::SetNextItemWidth(80.0f);
921 if (ImGui::Combo("##MenuTilemapPickerScale", &mode_idx, kLabels,
922 IM_ARRAYSIZE(kLabels))) {
923 picker_scale_mode_ = static_cast<gui::AdaptiveSheetScaleMode>(mode_idx);
924 }
925 }
927 available_width, picker_width_, picker_height_, picker_scale_mode_, 0.5f,
930 ImGui::SameLine();
931 ImGui::TextDisabled("%.2fx", layout.display_scale);
932
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();
938 if (grid_visible) {
939 auto result =
941 if ((result.tile_clicked || result.selection_changed) &&
942 result.selected_tile >= 0) {
944 }
945 }
946 ImGui::EndChild();
947
948 ImGui::Text("%s %d", tr("Tile:"), selected_tile_id_);
949 ImGui::SetNextItemWidth(140);
950 ImGui::SliderInt(tr("Palette"), &selected_palette_, 0, 7);
951 if (ImGui::IsItemDeactivatedAfterEdit())
953 ImGui::Checkbox(tr("H Flip"), &h_flip_);
954 ImGui::SameLine();
955 ImGui::Checkbox(tr("V Flip"), &v_flip_);
956 ImGui::Checkbox(tr("Priority"), &priority_);
957}
958
961 return;
963 uint16_t word = doc_.GetCellWord(hover_row_, hover_col_);
964 ImGui::Text("x=%d y=%d tile=%03X pal=%d H=%d V=%d P=%d word=%04X",
965 hover_col_, hover_row_, info.id_, info.palette_,
966 info.horizontal_mirror_ ? 1 : 0, info.vertical_mirror_ ? 1 : 0,
967 info.over_ ? 1 : 0, word);
968}
969
971 if (ImGui::CollapsingHeader(tr("Preview Overlays"))) {
972 ImGui::Checkbox(tr("Reference image"), &show_ref_overlay_);
973 if (show_ref_overlay_) {
974 ImGui::SameLine();
975 if (ImGui::Button("...##refimg")) {
977 options.filters.push_back({"PNG", "png"});
978 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
979 if (!path.empty()) {
980 ref_image_path_ = path;
981 auto bytes = util::ReadBinaryFile(path);
982 if (bytes.ok()) {
983 auto png = util::DecodePng(*bytes);
984 if (png.ok()) {
985 if (png->indexed) {
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];
993 }
994 SetReferenceImage(rgba, png->width, png->height);
995 } else {
996 SetReferenceImage(png->rgba, png->width, png->height);
997 }
998 }
999 }
1000 }
1001 }
1002 ImGui::SliderFloat(tr("Opacity"), &ref_opacity_, 0.0f, 1.0f);
1003 ImGui::SliderFloat("dx", &ref_nudge_.x, -32.0f, 32.0f);
1004 ImGui::SameLine();
1005 ImGui::SliderFloat("dy", &ref_nudge_.y, -32.0f, 32.0f);
1006 }
1007
1008 ImGui::Checkbox(tr("WRAM composite dump ($7E1000-17FF)"),
1010 if (show_wram_overlay_) {
1011 ImGui::SameLine();
1012 if (ImGui::Button("...##wramdump")) {
1014 options.filters.push_back({"WRAM dump", "bin"});
1015 std::string path = util::FileDialogWrapper::ShowOpenFileDialog(options);
1016 if (!path.empty()) {
1017 wram_dump_path_ = path;
1019 if (wram_doc.LoadFromFile(path).ok() && !chr_sheet_.empty()) {
1020 auto chr_fn = zelda3::MakeChrPixelFn(chr_sheet_);
1021 auto indexed = wram_doc.RenderIndexed(chr_fn);
1022 wram_bitmap_.Create(wram_doc.render_width(),
1023 wram_doc.render_height(), 8, indexed);
1024 wram_bitmap_.SetPalette(BuildSnesPalette(palette_colors_));
1027 wram_loaded_ = true;
1028 }
1029 }
1030 }
1031 }
1032
1033 if (ImGui::Checkbox(tr("Dynamic content (page 3 mask icons, read-only)"),
1036 }
1037 if (show_dynamic_layer_) {
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)."));
1042 }
1043 }
1044}
1045
1047 ImGui::Text("%s", doc_.dirty() ? tr("Modified") : tr("Saved"));
1048 if (doc_.has_backup()) {
1049 ImGui::SameLine();
1050 ImGui::TextDisabled("(backup: %s)", doc_.backup_path().c_str());
1051 }
1052}
1053
1054void MenuTilemapEditorUI::BeginStroke(const std::string& description) {
1055 stroke_active_ = true;
1057 stroke_description_ = description;
1058}
1059
1061 stroke_active_ = false;
1062 if (undo_manager_ == nullptr)
1063 return;
1064 ScreenSnapshot before;
1066 before.menu_tilemap.path = doc_.path();
1068
1069 ScreenSnapshot after;
1071 after.menu_tilemap.path = doc_.path();
1072 after.menu_tilemap.bytes = doc_.raw_bytes();
1073
1074 if (before.menu_tilemap.bytes == after.menu_tilemap.bytes)
1075 return;
1076
1077 undo_manager_->Push(std::make_unique<ScreenEditAction>(
1078 before, after,
1079 [this](const ScreenSnapshot& snap) {
1081 },
1083}
1084
1085void MenuTilemapEditorUI::RestoreSnapshot(const std::string& path,
1086 const std::vector<uint8_t>& bytes) {
1087 // The shared undo stack outlives the open file: a snapshot taken on one
1088 // tilemap must never be applied to another that was opened afterwards.
1089 if (path != doc_.path())
1090 return;
1091 if (doc_.RestoreBytes(bytes).ok()) {
1093 }
1094}
1095
1096void MenuTilemapEditorUI::PaintCellAt(int row, int col) {
1097 gfx::TileInfo info(static_cast<uint16_t>(selected_tile_id_),
1098 static_cast<uint8_t>(selected_palette_), v_flip_, h_flip_,
1099 priority_);
1100 doc_.SetCell(row, col, info);
1101}
1102
1103void MenuTilemapEditorUI::EyedropAt(int row, int col) {
1104 gfx::TileInfo info = doc_.GetCell(row, col);
1105 selected_tile_id_ = info.id_;
1109 priority_ = info.over_;
1110}
1111
1112} // namespace editor
1113} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
bool is_loaded() const
Definition rom.h:187
absl::Status ResolvePaletteSource(Rom *rom, zelda3::GameData *game_data, std::string *description)
void BeginStroke(const std::string &description)
gui::AdaptiveSheetScaleMode picker_scale_mode_
zelda3::MenuTilemapDocument::Clipboard clipboard_
void DrawSourceBar(Rom *rom, zelda3::GameData *game_data, project::YazeProject *project)
void RestoreSnapshot(const std::string &path, const std::vector< uint8_t > &bytes)
zelda3::MenuTilemapDocument::Rect selection_
void Draw(Rom *rom, zelda3::GameData *game_data, project::YazeProject *project, UndoManager *undo_manager)
void RefreshProjectFileList(project::YazeProject *project)
std::vector< FileListEntry > project_files_
absl::Status ResolveSources(Rom *rom, zelda3::GameData *game_data)
void SetReferenceImage(const std::vector< uint8_t > &rgba, int width, int height)
void DrawFileBar(Rom *rom, project::YazeProject *project)
std::array< gfx::SnesColor, 32 > palette_colors_
absl::Status LoadFile(const std::string &path)
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)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
bool is_active() const
Definition bitmap.h:414
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1293
auto hover_mouse_pos() const
Definition canvas.h:476
void DrawContextMenu()
Definition canvas.cc:823
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1229
void SetCanvasSize(ImVec2 canvas_size)
Definition canvas.h:388
bool IsMouseHovering() const
Definition canvas.h:354
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:702
void SetGlobalScale(float scale)
Definition canvas.h:394
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1620
RAII guard for ImGui style colors.
Definition style_guard.h:27
void SetDisplayScale(float display_scale)
RenderResult Render(gfx::Bitmap &atlas, bool atlas_ready)
static float CurrentScrollbarSize()
Current style scrollbar size, or ImGui's default without a context.
void SetTilesPerRow(int tiles_per_row)
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...
std::vector< uint8_t > RenderIndexed(const ChrPixelFn &chr) const
const std::vector< uint8_t > & raw_bytes() const
absl::StatusOr< bool > ExternalChangeDetected() const
bool SetCell(int row, int col, gfx::TileInfo info)
Clipboard CopyRect(Rect rect) const
absl::Status LoadFromFile(const std::string &path)
int FillRect(Rect rect, gfx::TileInfo info)
const std::string & backup_path() const
absl::Status RestoreBytes(std::vector< uint8_t > bytes)
gfx::TileInfo GetCell(int row, int col) const
uint16_t GetCellWord(int row, int col) const
int EraseRect(Rect rect, uint16_t erase_word=0x0000)
bool InBounds(int row, int col) const
int PasteRect(int row, int col, const Clipboard &clip)
absl::Status SaveAs(const std::string &path)
const std::string & path() const
const AgentUITheme & GetTheme()
gfx::SnesPalette BuildSnesPalette(const std::array< gfx::SnesColor, 32 > &colors)
std::vector< std::array< uint8_t, 4 > > BuildRgbaPalette(const std::array< gfx::SnesColor, 32 > &colors)
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
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.
Definition file_util.cc:19
absl::StatusOr< PngImage > DecodePng(const std::vector< uint8_t > &bytes)
std::string GetFileExtension(const std::string &filename)
Gets the file extension from a filename.
Definition file_util.cc:15
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)
Definition snes.cc:22
Unified screen editor snapshot.
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::string symbols_filename
Definition project.h:315
std::vector< FileDialogFilter > filters
Definition file_util.h:17