yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_editor.cc
Go to the documentation of this file.
1#include "message_editor.h"
2
3#include <algorithm>
4#include <string>
5#include <unordered_map>
6#include <vector>
7
8#include "absl/status/status.h"
9#include "absl/strings/str_cat.h"
10#include "absl/strings/str_format.h"
13#include "app/gfx/core/bitmap.h"
19#include "app/gui/core/icons.h"
20#include "app/gui/core/input.h"
21#include "app/gui/core/style.h"
23#include "imgui.h"
24#include "imgui/misc/cpp/imgui_stdlib.h"
25#include "rom/rom.h"
26#include "util/file_util.h"
27#include "util/hex.h"
28#include "util/log.h"
29
30namespace yaze {
31namespace editor {
32
33namespace {
34std::string DisplayTextOverflowError(int pos, bool bank) {
35 int space = bank ? kTextDataEnd - kTextData : kTextData2End - kTextData2;
36 std::string bankSTR = bank ? "1st" : "2nd";
37 std::string posSTR =
38 bank ? absl::StrFormat("%X4", pos & 0xFFFF)
39 : absl::StrFormat("%X4", (pos - kTextData2) & 0xFFFF);
40 std::string message = absl::StrFormat(
41 "There is too much text data in the %s block to save.\n"
42 "Available: %X4 | Used: %s",
43 bankSTR, space, posSTR);
44 return message;
45}
46} // namespace
47
48using ImGui::BeginChild;
49using ImGui::BeginTable;
50using ImGui::Button;
51using ImGui::EndChild;
52using ImGui::EndTable;
53using ImGui::InputTextMultiline;
54using ImGui::PopID;
55using ImGui::PushID;
56using ImGui::SameLine;
57using ImGui::Separator;
58using ImGui::TableHeadersRow;
59using ImGui::TableNextColumn;
60using ImGui::TableSetupColumn;
61using ImGui::Text;
62using ImGui::TextWrapped;
63
64constexpr ImGuiTableFlags kMessageTableFlags = ImGuiTableFlags_Hideable |
65 ImGuiTableFlags_Borders |
66 ImGuiTableFlags_Resizable;
67
70 // Register panels with WorkspaceWindowManager (dependency injection)
72 return;
73
74 auto* window_manager = dependencies_.window_manager;
75 const size_t session_id = dependencies_.session_id;
76
77 // Register WindowContent implementations (they provide both metadata and drawing)
78 window_manager->RegisterWindowContent(
79 std::make_unique<MessageListPanel>([this]() { DrawMessageList(); }));
80 window_manager->RegisterWindowContent(
81 std::make_unique<MessageEditorPanel>([this]() { DrawCurrentMessage(); }));
82 window_manager->RegisterWindowContent(
83 std::make_unique<FontAtlasPanel>([this]() {
86 }));
87 window_manager->RegisterWindowContent(
88 std::make_unique<DictionaryPanel>([this]() {
92 }));
93
94 // Show message list by default
95 window_manager->OpenWindow(session_id, "message.message_list");
96
97 for (int i = 0; i < kWidthArraySize; i++) {
99 }
100
102 list_of_texts_ = ReadAllTextData(rom()->mutable_data());
103 LOG_INFO("MessageEditor", "Loaded %zu messages from ROM",
104 list_of_texts_.size());
105
110 }
113
115
116 if (!list_of_texts_.empty()) {
117 // Default to message 1 if available, otherwise 0
118 size_t default_idx = list_of_texts_.size() > 1 ? 1 : 0;
119 current_message_ = list_of_texts_[default_idx];
124 } else {
125 LOG_ERROR("MessageEditor", "No messages found in ROM!");
126 }
127}
128
129bool MessageEditor::OpenMessageById(int display_id) {
130 const int vanilla_count = static_cast<int>(list_of_texts_.size());
131 const int expanded_base_id = expanded_message_base_id_;
132
133 int expanded_count = static_cast<int>(expanded_messages_.size());
134 auto resolved = ResolveMessageDisplayId(display_id, vanilla_count,
135 expanded_base_id, expanded_count);
136
137 // Convenience: if an expanded ID is requested but we haven't loaded expanded
138 // messages yet, try loading from ROM once.
139 if (!resolved.has_value() && expanded_count == 0 &&
140 display_id >= expanded_base_id && rom_ && rom_->is_loaded()) {
141 const int start = GetExpandedTextDataStart();
142 const int end = GetExpandedTextDataEnd();
143 const size_t rom_size = rom_->size();
144 if (start >= 0 && end >= start && static_cast<size_t>(end) < rom_size) {
145 const auto status = LoadExpandedMessagesFromRom();
146 if (!status.ok()) {
147 LOG_DEBUG("MessageEditor",
148 "OpenMessageById: expanded load skipped/failed: %s",
149 std::string(status.message()).c_str());
150 }
151 expanded_count = static_cast<int>(expanded_messages_.size());
152 resolved = ResolveMessageDisplayId(display_id, vanilla_count,
153 expanded_base_id, expanded_count);
154 } else {
155 LOG_DEBUG("MessageEditor",
156 "OpenMessageById: expanded region out of bounds (0x%X-0x%X, "
157 "rom=0x%zX)",
158 start, end, rom_size);
159 }
160 }
161
162 if (!resolved.has_value()) {
163 return false;
164 }
165
167 const size_t session_id = dependencies_.session_id;
169 "message.message_list");
171 "message.message_editor");
172 }
173
174 if (!resolved->is_expanded) {
175 const int idx = resolved->index;
176 if (idx < 0 || idx >= vanilla_count) {
177 return false;
178 }
179
180 const auto& message = list_of_texts_[idx];
181 current_message_ = message;
182 current_message_index_ = message.ID;
184
185 const int parsed_idx = resolved->display_id;
186 if (parsed_idx >= 0 &&
187 parsed_idx < static_cast<int>(parsed_messages_.size())) {
189 } else {
190 message_text_box_.text.clear();
191 }
192
194 return true;
195 }
196
197 // Expanded message.
198 const int idx = resolved->index;
199 if (idx < 0 || idx >= expanded_count) {
200 return false;
201 }
202
203 const auto& message = expanded_messages_[idx];
204 current_message_ = message;
205 current_message_index_ = message.ID;
207
208 const int parsed_idx = resolved->display_id;
209 if (parsed_idx >= 0 &&
210 parsed_idx < static_cast<int>(parsed_messages_.size())) {
212 } else {
213 message_text_box_.text.clear();
214 }
215
217 return true;
218}
219
221 int base_id = static_cast<int>(list_of_texts_.size());
223 const auto& layout = dependencies_.project->hack_manifest.message_layout();
224 if (layout.first_expanded_id != 0) {
225 base_id = static_cast<int>(layout.first_expanded_id);
226 }
227 }
228
229 // Never allow the expanded base to precede the vanilla message count; this
230 // prevents truncating/overlapping IDs when the manifest is missing/mistyped.
231 base_id = std::max(base_id, static_cast<int>(list_of_texts_.size()));
232 return base_id;
233}
234
236 if (game_data() && !game_data()->palette_groups.hud.empty()) {
238 }
239
242 }
243}
244
246 std::vector<gfx::SnesColor> colors;
247 colors.reserve(16);
248 for (int i = 0; i < 16; ++i) {
249 const float value = static_cast<float>(i) / 15.0f;
250 colors.emplace_back(ImVec4(value, value, value, 1.0f));
251 }
252
253 if (!colors.empty()) {
254 colors[0].set_transparent(true);
255 }
256
257 return gfx::SnesPalette(colors);
258}
259
262 if (!rom() || !rom()->is_loaded()) {
263 LOG_WARN("MessageEditor", "ROM not loaded - skipping font graphics load");
264 return;
265 }
266
267 std::fill(raw_font_gfx_data_.begin(), raw_font_gfx_data_.end(), 0);
268
269 const size_t rom_size = rom()->size();
270 if (rom_size > static_cast<size_t>(kGfxFont)) {
271 const size_t available = std::min(raw_font_gfx_data_.size(),
272 rom_size - static_cast<size_t>(kGfxFont));
273 std::copy_n(rom()->data() + kGfxFont, available,
275 if (available < raw_font_gfx_data_.size()) {
276 LOG_WARN("MessageEditor",
277 "Font graphics truncated (ROM size %zu, read %zu bytes)",
278 rom_size, available);
279 }
280 } else {
281 LOG_WARN("MessageEditor",
282 "ROM size %zu too small for font graphics offset 0x%X", rom_size,
283 kGfxFont);
284 }
285
287 gfx::SnesTo8bppSheet(raw_font_gfx_data_, /*bpp=*/2, /*num_sheets=*/2);
288
289 auto load_font = zelda3::LoadFontGraphics(*rom());
290 if (load_font.ok()) {
291 message_preview_.font_gfx16_data_2_ = load_font.value().vector();
292 } else {
293 const std::string error_message(load_font.status().message());
294 LOG_WARN("MessageEditor", "LoadFontGraphics failed: %s",
295 error_message.c_str());
296 }
297
298 const auto& font_data = !message_preview_.font_gfx16_data_.empty()
301 RefreshFontAtlasBitmap(font_data);
303}
304
306 const std::vector<uint8_t>& font_data) {
307 if (font_data.empty()) {
308 LOG_WARN("MessageEditor", "Font graphics data missing - atlas stays empty");
309 return;
310 }
311
312 const int atlas_width = kFontGfxMessageSize;
313 const size_t row_count = (font_data.size() + atlas_width - 1) / atlas_width;
314 const int atlas_height = static_cast<int>(std::max<size_t>(1, row_count));
315
316 const size_t expected_size = static_cast<size_t>(atlas_width) * atlas_height;
317 std::vector<uint8_t> padded(font_data.begin(), font_data.end());
318 if (padded.size() < expected_size) {
319 padded.resize(expected_size, 0);
320 } else if (padded.size() > expected_size) {
321 padded.resize(expected_size);
322 }
323
324 font_gfx_bitmap_.Create(atlas_width, atlas_height, kFontGfxMessageDepth,
325 padded);
328 }
331}
332
333absl::Status MessageEditor::Load() {
334 gfx::ScopedTimer timer("MessageEditor::Load");
335 return absl::OkStatus();
336}
337
338absl::Status MessageEditor::Update() {
339 // Panel drawing is handled centrally by WorkspaceWindowManager::DrawAllVisiblePanels()
340 // via the WindowContent implementations registered in Initialize().
341 // No local drawing needed here.
342 return absl::OkStatus();
343}
344
349
352
354 return;
355 }
356
357 auto queue_refresh = [](gfx::Bitmap& bitmap) {
358 if (!bitmap.is_active()) {
359 return;
360 }
361 const auto command = bitmap.texture()
364 gfx::Arena::Get().QueueTextureCommand(command, &bitmap);
365 };
366
368 queue_refresh(font_gfx_bitmap_);
369
372 queue_refresh(current_font_gfx16_bitmap_);
373 }
374}
375
394
397 if (BeginChild("##MessagesList", ImVec2(0, 0), true,
398 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
400 if (ImGui::Button("Import Bundle")) {
402 if (!path.empty()) {
404 }
405 }
406 ImGui::SameLine();
407 if (ImGui::Button("Export Bundle")) {
409 if (!path.empty()) {
410 auto status =
412 if (!status.ok()) {
414 absl::StrFormat("Export failed: %s", status.message());
416 } else {
417 message_bundle_status_ = absl::StrFormat("Exported bundle: %s", path);
419 }
420 }
421 }
422 if (!message_bundle_status_.empty()) {
425 ImGui::TextColored(color, "%s", message_bundle_status_.c_str());
426 }
427 ImGui::Separator();
428 if (BeginTable("##MessagesTable", 4, kMessageTableFlags)) {
429 TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 50);
430 TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80);
431 TableSetupColumn("Contents", ImGuiTableColumnFlags_WidthStretch);
432 TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 100);
433
434 TableHeadersRow();
435
436 // Calculate total rows for clipper
437 const int vanilla_count = static_cast<int>(list_of_texts_.size());
438 const int expanded_count = static_cast<int>(expanded_messages_.size());
439 const int total_rows = vanilla_count + expanded_count;
440
441 // Use ImGuiListClipper for virtualized rendering
442 ImGuiListClipper clipper;
443 clipper.Begin(total_rows);
444
445 while (clipper.Step()) {
446 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
447 if (row < vanilla_count) {
448 // Vanilla message
449 const auto& message = list_of_texts_[row];
450 TableNextColumn();
451 PushID(message.ID);
452 if (Button(util::HexWord(message.ID).c_str())) {
454 current_message_ = message;
455 current_message_index_ = message.ID;
459 }
460 PopID();
461
462 TableNextColumn();
463 ImGui::TextColored(gui::GetInfoColor(), "Vanilla");
464
465 TableNextColumn();
466 TextWrapped("%s", parsed_messages_[message.ID].c_str());
467
468 TableNextColumn();
469 TextWrapped("%s", util::HexLong(message.Address).c_str());
470 } else {
471 // Expanded message
472 int expanded_idx = row - vanilla_count;
473 const auto& expanded_message = expanded_messages_[expanded_idx];
474 const int display_id =
475 expanded_message_base_id_ + expanded_message.ID;
476 const char* display_text = "Missing text";
477 if (display_id >= 0 &&
478 display_id < static_cast<int>(parsed_messages_.size())) {
479 display_text = parsed_messages_[display_id].c_str();
480 }
481 TableNextColumn();
482 PushID(display_id);
483 if (Button(util::HexWord(display_id).c_str())) {
485 current_message_ = expanded_message;
486 current_message_index_ = expanded_message.ID;
488 message_text_box_.text = display_text;
490 }
491 PopID();
492
493 TableNextColumn();
494 ImGui::TextColored(gui::GetWarningColor(), "Expanded");
495
496 TableNextColumn();
497 TextWrapped("%s", display_text);
498
499 TableNextColumn();
500 TextWrapped("%s", util::HexLong(expanded_message.Address).c_str());
501 }
502 }
503 }
504
505 EndTable();
506 }
507 }
508 EndChild();
509}
510
512 Button(absl::StrCat("Message ", current_message_.ID).c_str());
513 if (InputTextMultiline("##MessageEditor", &message_text_box_.text,
514 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
516 }
517 if (ImGui::IsItemDeactivatedAfterEdit()) {
519 }
521 if (!line_warnings.empty()) {
522 ImGui::TextColored(gui::GetWarningColor(), "Line width warnings");
523 for (const auto& warning : line_warnings) {
524 ImGui::BulletText("%s", warning.c_str());
525 }
526 }
527 Separator();
529
530 ImGui::BeginChild("##MessagePreview", ImVec2(0, 0), true);
532 Text("Message Preview");
533 if (Button("View Palette")) {
534 ImGui::OpenPopup("Palette");
535 }
536 if (ImGui::BeginPopup("Palette")) {
538 ImGui::EndPopup();
539 }
541 BeginChild("CurrentGfxFont", ImVec2(348, 0), true,
542 ImGuiWindowFlags_NoScrollWithMouse);
546
547 // Handle mouse wheel scrolling
548 if (ImGui::IsWindowHovered()) {
549 float wheel = ImGui::GetIO().MouseWheel;
550 if (wheel > 0 && message_preview_.shown_lines > 0) {
552 } else if (wheel < 0 &&
555 }
556 }
557
558 // Draw only the visible portion of the text
559 const ImVec2 preview_canvas_size = current_font_gfx16_canvas_.canvas_size();
560 const float dest_width = std::max(0.0f, preview_canvas_size.x - 8.0f);
561 const float dest_height = std::max(0.0f, preview_canvas_size.y - 8.0f);
562 float src_height = 0.0f;
563 if (dest_width > 0.0f && dest_height > 0.0f) {
564 const float src_width =
565 std::min(dest_width * 0.5f, static_cast<float>(kCurrentMessageWidth));
566 src_height =
567 std::min(dest_height * 0.5f, static_cast<float>(kCurrentMessageHeight));
569 current_font_gfx16_bitmap_, ImVec2(0, 0), // Destination position
570 ImVec2(dest_width, dest_height), // Destination size
571 ImVec2(0, message_preview_.shown_lines * 16), // Source position
572 ImVec2(src_width, src_height) // Source size
573 );
574 }
575
576 // Draw scroll break separator lines on the preview canvas
577 {
578 ImDrawList* overlay_draw_list = ImGui::GetWindowDrawList();
579 ImVec2 canvas_p0 = current_font_gfx16_canvas_.zero_point();
580 ImVec2 canvas_sz = current_font_gfx16_canvas_.canvas_size();
581 float line_height = 16.0f;
582 // The bitmap is drawn scaled: dest occupies full canvas, so compute the
583 // vertical scale factor from destination height to source height.
584 float scale_y = 1.0f;
585 if (dest_height > 0.0f && src_height > 0.0f) {
586 scale_y = dest_height / src_height;
587 }
588 for (int marker_line : message_preview_.scroll_marker_lines) {
589 float src_y = (marker_line - message_preview_.shown_lines) * line_height;
590 float y = canvas_p0.y + src_y * scale_y;
591 if (y >= canvas_p0.y && y <= canvas_p0.y + canvas_sz.y) {
592 overlay_draw_list->AddLine(ImVec2(canvas_p0.x, y),
593 ImVec2(canvas_p0.x + canvas_sz.x, y),
594 IM_COL32(100, 180, 255, 180), 1.5f);
595 overlay_draw_list->AddText(ImVec2(canvas_p0.x + canvas_sz.x + 4, y - 6),
596 IM_COL32(100, 180, 255, 200), "[V]");
597 }
598 }
599 }
600
603 EndChild();
604
605 // Message Structure info panel
606 if (ImGui::CollapsingHeader("Message Structure",
607 ImGuiTreeNodeFlags_DefaultOpen)) {
608 ImGui::Text("Lines: %d", message_preview_.text_line + 1);
609
610 int scroll_count = 0;
611 int current_line_chars = 0;
612 int line_num = 0;
613
614 for (size_t i = 0; i < current_message_.Data.size(); i++) {
615 uint8_t byte = current_message_.Data[i];
616 if (byte == kScrollVertical) {
617 scroll_count++;
618 ImGui::TextColored(gui::GetInfoColor(),
619 " [V] Scroll at byte %zu (line %d, %d chars)", i,
620 line_num, current_line_chars);
621 current_line_chars = 0;
622 line_num++;
623 } else if (byte == kLine1) {
624 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
625 " [1] Line 1 at byte %zu", i);
626 current_line_chars = 0;
627 line_num = 0;
628 } else if (byte == kLine2) {
629 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
630 " [2] Line 2 at byte %zu", i);
631 current_line_chars = 0;
632 line_num = 1;
633 } else if (byte == kLine3) {
634 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
635 " [3] Line 3 at byte %zu", i);
636 current_line_chars = 0;
637 line_num = 2;
638 } else if (byte < 100) {
639 current_line_chars++;
640 }
641 }
642
643 if (scroll_count == 0) {
644 ImGui::TextDisabled("No scroll breaks in this message");
645 } else {
646 ImGui::Text("Total scroll breaks: %d", scroll_count);
647 }
648
649 // Character width budget
650 ImGui::Separator();
651 ImGui::TextDisabled("Line width budget (max ~170px):");
652 int estimated_line_width = current_line_chars * 8;
653 float width_ratio = static_cast<float>(estimated_line_width) / 170.0f;
654 ImVec4 width_color = (width_ratio > 1.0f) ? gui::GetErrorColor()
655 : (width_ratio > 0.85f) ? gui::GetWarningColor()
657 ImGui::TextColored(width_color, "Last line: ~%dpx / 170px (%d chars)",
658 estimated_line_width, current_line_chars);
659 }
660
661 ImGui::EndChild();
662}
663
671
673 ImGui::BeginChild("##ExpandedMessageSettings", ImVec2(0, 130), true,
674 ImGuiWindowFlags_AlwaysVerticalScrollbar);
675 ImGui::Text("Expanded Messages");
676
677 if (ImGui::Button("Load from ROM")) {
678 auto status = LoadExpandedMessagesFromRom();
679 if (!status.ok()) {
680 LOG_WARN("MessageEditor", "Load from ROM: %s",
681 std::string(status.message()).c_str());
682 }
683 }
684 ImGui::SameLine();
685 if (ImGui::Button("Load from File")) {
687 if (!path.empty()) {
691 expanded_messages_.clear();
692 std::vector<std::string> parsed_expanded;
694 parsed_expanded, expanded_messages_,
696 if (!status.ok()) {
697 if (auto* popup_manager = dependencies_.popup_manager) {
698 popup_manager->Show("Error");
699 }
700 } else {
701 parsed_messages_.insert(parsed_messages_.end(), parsed_expanded.begin(),
702 parsed_expanded.end());
703 }
704 }
705 }
706
707 if (expanded_messages_.size() > 0) {
708 ImGui::Text("Source: %s", expanded_message_path_.c_str());
709 ImGui::Text("Messages: %lu", expanded_messages_.size());
710
711 // Capacity indicator
712 int capacity = GetExpandedTextDataEnd() - GetExpandedTextDataStart() + 1;
713 int used = CalculateExpandedBankUsage();
714 int remaining = capacity - used;
715 float usage_ratio = static_cast<float>(used) / static_cast<float>(capacity);
716
717 ImVec4 capacity_color;
718 if (usage_ratio < 0.75f) {
719 capacity_color = gui::GetSuccessColor();
720 } else if (usage_ratio < 0.90f) {
721 capacity_color = gui::GetWarningColor();
722 } else {
723 capacity_color = gui::GetErrorColor();
724 }
725 ImGui::TextColored(capacity_color, "Bank: %d / %d bytes (%d free)", used,
726 capacity, remaining);
727
728 if (ImGui::Button("Add New Message")) {
729 MessageData new_message;
730 new_message.ID = expanded_messages_.back().ID + 1;
731 new_message.Address = expanded_messages_.back().Address +
732 expanded_messages_.back().Data.size();
733 expanded_messages_.push_back(new_message);
734 const int display_id = expanded_message_base_id_ + new_message.ID;
735 if (display_id >= 0 &&
736 static_cast<size_t>(display_id) >= parsed_messages_.size()) {
737 parsed_messages_.resize(display_id + 1);
738 }
739 }
740
741 ImGui::SameLine();
742 if (ImGui::Button("Export to JSON")) {
744 if (!path.empty()) {
746 }
747 }
748 }
749
750 EndChild();
751}
752
754 ImGui::BeginChild("##TextCommands",
755 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
756 ImGuiWindowFlags_AlwaysVerticalScrollbar);
757 static uint8_t command_parameter = 0;
758 gui::InputHexByte("Command Parameter", &command_parameter);
759 for (const auto& text_element : TextCommands) {
760 if (Button(text_element.GenericToken.c_str())) {
761 message_text_box_.text.append(
762 text_element.GetParamToken(command_parameter));
764 }
765 SameLine();
766 TextWrapped("%s", text_element.Description.c_str());
767 Separator();
768 }
769 EndChild();
770}
771
773 ImGui::BeginChild("##SpecialChars",
774 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
775 ImGuiWindowFlags_AlwaysVerticalScrollbar);
776 for (const auto& text_element : SpecialChars) {
777 if (Button(text_element.GenericToken.c_str())) {
778 message_text_box_.text.append(text_element.GenericToken);
780 }
781 SameLine();
782 TextWrapped("%s", text_element.Description.c_str());
783 Separator();
784 }
785 EndChild();
786}
787
789 if (ImGui::BeginChild("##DictionaryChild",
790 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
791 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
792 if (BeginTable("##Dictionary", 2, kMessageTableFlags)) {
793 TableSetupColumn("ID");
794 TableSetupColumn("Contents");
795 TableHeadersRow();
796
797 // Use ImGuiListClipper for virtualized rendering
798 const int dict_count =
799 static_cast<int>(message_preview_.all_dictionaries_.size());
800 ImGuiListClipper clipper;
801 clipper.Begin(dict_count);
802
803 while (clipper.Step()) {
804 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
805 const auto& dictionary = message_preview_.all_dictionaries_[row];
806 TableNextColumn();
807 Text("%s", util::HexWord(dictionary.ID).c_str());
808 TableNextColumn();
809 Text("%s", dictionary.Contents.c_str());
810 }
811 }
812
813 EndTable();
814 }
815 }
816 EndChild();
817}
818
819void MessageEditor::UpdateCurrentMessageFromText(const std::string& text) {
821
822 std::string raw_text = text;
823 raw_text.erase(std::remove(raw_text.begin(), raw_text.end(), '\n'),
824 raw_text.end());
825
826 current_message_.RawString = raw_text;
829
830 int parsed_index = current_message_index_;
833 }
834
835 if (parsed_index >= 0) {
836 if (static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
837 parsed_messages_.resize(parsed_index + 1);
838 }
839 parsed_messages_[parsed_index] = text;
840 }
841
843 if (current_message_index_ >= 0 &&
844 current_message_index_ < static_cast<int>(expanded_messages_.size())) {
846 }
847 } else {
848 if (current_message_index_ >= 0 &&
849 current_message_index_ < static_cast<int>(list_of_texts_.size())) {
851 }
852 }
853
855}
856
857void MessageEditor::ImportMessageBundleFromFile(const std::string& path) {
860
861 auto entries_or = LoadMessageBundleFromJson(path);
862 if (!entries_or.ok()) {
864 absl::StrFormat("Import failed: %s", entries_or.status().message());
866 return;
867 }
868
869 int applied = 0;
870 int errors = 0;
871 int warnings = 0;
872 int duplicate_errors = 0;
873 int parse_error_entries = 0;
874 int vanilla_updated = 0;
875 int expanded_updated = 0;
876 int expanded_created = 0;
877 bool expanded_modified = false;
878 std::vector<std::string> issue_samples;
879
880 auto add_issue_sample = [&issue_samples](const std::string& issue) {
881 constexpr size_t kMaxIssueSamples = 4;
882 if (issue_samples.size() < kMaxIssueSamples) {
883 issue_samples.push_back(issue);
884 }
885 };
886
887 auto make_entry_key = [](const MessageBundleEntry& entry) {
888 return absl::StrFormat("%s:%d", MessageBankToString(entry.bank), entry.id);
889 };
890
891 std::unordered_map<std::string, int> seen_entries;
892
893 auto entries = entries_or.value();
894 for (const auto& entry : entries) {
895 const std::string entry_key = make_entry_key(entry);
896 if (seen_entries.find(entry_key) != seen_entries.end()) {
897 errors++;
898 duplicate_errors++;
899 add_issue_sample(absl::StrFormat("Duplicate entry for %s", entry_key));
900 continue;
901 }
902 seen_entries.emplace(entry_key, 1);
903
904 auto parse_result = ParseMessageToDataWithDiagnostics(entry.text);
905 auto line_warnings = ValidateMessageLineWidths(entry.text);
906 warnings += static_cast<int>(parse_result.warnings.size());
907 warnings += static_cast<int>(line_warnings.size());
908
909 if (!parse_result.ok()) {
910 errors++;
911 parse_error_entries++;
912 if (!parse_result.errors.empty()) {
913 add_issue_sample(absl::StrFormat("Parse error for %s: %s", entry_key,
914 parse_result.errors.front()));
915 } else {
916 add_issue_sample(absl::StrFormat("Parse error for %s", entry_key));
917 }
918 continue;
919 }
920
921 if (entry.bank == MessageBank::kVanilla) {
922 if (entry.id < 0 || entry.id >= static_cast<int>(list_of_texts_.size())) {
923 errors++;
924 add_issue_sample(
925 absl::StrFormat("Vanilla ID out of range: %d", entry.id));
926 continue;
927 }
928 auto& message = list_of_texts_[entry.id];
929 message.RawString = entry.text;
930 message.ContentsParsed = entry.text;
931 message.Data = parse_result.bytes;
932 message.DataParsed = parse_result.bytes;
933 if (entry.id >= 0 &&
934 entry.id < static_cast<int>(parsed_messages_.size())) {
935 parsed_messages_[entry.id] = entry.text;
936 }
937 vanilla_updated++;
938 applied++;
939 } else {
940 if (entry.id < 0) {
941 errors++;
942 add_issue_sample(
943 absl::StrFormat("Expanded ID out of range: %d", entry.id));
944 continue;
945 }
946 if (entry.id >= static_cast<int>(expanded_messages_.size())) {
947 const int old_size = static_cast<int>(expanded_messages_.size());
948 const int target_size = entry.id + 1;
949 expanded_messages_.resize(target_size);
950 for (int i = old_size; i < target_size; ++i) {
951 expanded_messages_[i].ID = i;
952 }
953 expanded_created += target_size - old_size;
954 }
955 auto& message = expanded_messages_[entry.id];
956 message.RawString = entry.text;
957 message.ContentsParsed = entry.text;
958 message.Data = parse_result.bytes;
959 message.DataParsed = parse_result.bytes;
960 const int parsed_index = expanded_message_base_id_ + entry.id;
961 if (parsed_index >= 0) {
962 if (static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
963 parsed_messages_.resize(parsed_index + 1);
964 }
965 parsed_messages_[parsed_index] = entry.text;
966 }
967 expanded_modified = true;
968 expanded_updated++;
969 applied++;
970 }
971 }
972
973 if (expanded_modified) {
974 int pos = GetExpandedTextDataStart();
975 for (auto& message : expanded_messages_) {
976 message.Address = pos;
977 pos += static_cast<int>(message.Data.size()) + 1;
978 }
979 }
980
981 int current_display_id = current_message_index_;
984 }
985 if (current_display_id >= 0) {
986 OpenMessageById(current_display_id);
987 }
988
989 if (errors > 0) {
990 message_bundle_status_ = absl::StrFormat(
991 "Import finished with %d errors (%d applied: vanilla %d updated, "
992 "expanded %d updated/%d created; %d warnings, %d duplicates, %d "
993 "parse failures).",
994 errors, applied, vanilla_updated, expanded_updated, expanded_created,
995 warnings, duplicate_errors, parse_error_entries);
996 if (!issue_samples.empty()) {
997 message_bundle_status_ = absl::StrFormat(
998 "%s Example: %s", message_bundle_status_, issue_samples.front());
999 }
1001 } else {
1002 message_bundle_status_ = absl::StrFormat(
1003 "Imported %d messages (vanilla %d updated, expanded %d updated/%d "
1004 "created, %d warnings).",
1005 applied, vanilla_updated, expanded_updated, expanded_created, warnings);
1006 }
1007}
1008
1010 // Render the message to the preview bitmap
1012
1013 // Validate preview data before updating
1015 LOG_WARN("MessageEditor", "Preview data is empty, skipping bitmap update");
1016 return;
1017 }
1018
1020 // CRITICAL: Use set_data() to properly update both data_ AND surface_
1021 // mutable_data() returns a reference but doesn't update the surface!
1023
1027 }
1028
1029 // Validate surface was updated
1031 LOG_ERROR("MessageEditor", "Bitmap surface is null after set_data()");
1032 return;
1033 }
1034
1035 // Queue texture update (or create if missing) so changes are visible
1036 const auto command = current_font_gfx16_bitmap_.texture()
1040
1041 LOG_DEBUG(
1042 "MessageEditor",
1043 "Updated message preview bitmap (size: %zu) and queued texture update",
1045 } else {
1046 // Create bitmap and queue texture creation with 8-bit indexed depth
1053
1054 LOG_INFO("MessageEditor",
1055 "Created message preview bitmap (%dx%d) with 8-bit depth and "
1056 "queued texture creation",
1058 }
1059}
1060
1061absl::Status MessageEditor::Save() {
1062 std::vector<uint8_t> backup = rom()->vector();
1063
1064 for (int i = 0; i < kWidthArraySize; i++) {
1065 RETURN_IF_ERROR(rom()->WriteByte(kCharactersWidth + i,
1067 }
1068
1069 int pos = kTextData;
1070 bool in_second_bank = false;
1071
1072 for (const auto& message : list_of_texts_) {
1073 for (const auto value : message.Data) {
1074 RETURN_IF_ERROR(rom()->WriteByte(pos, value));
1075
1076 if (value == kBlockTerminator) {
1077 // Make sure we didn't go over the space available in the first block.
1078 // 0x7FFF available.
1079 if (!in_second_bank && pos > kTextDataEnd) {
1080 return absl::InternalError(DisplayTextOverflowError(pos, true));
1081 }
1082
1083 // Switch to the second block.
1084 pos = kTextData2 - 1;
1085 in_second_bank = true;
1086 }
1087
1088 pos++;
1089 }
1090
1091 RETURN_IF_ERROR(rom()->WriteByte(pos++, kMessageTerminator));
1092 }
1093
1094 // Verify that we didn't go over the space available for the second block.
1095 // 0x14BF available.
1096 if (in_second_bank && pos > kTextData2End) {
1097 std::copy(backup.begin(), backup.end(), rom()->mutable_data());
1098 return absl::InternalError(DisplayTextOverflowError(pos, false));
1099 }
1100
1101 RETURN_IF_ERROR(rom()->WriteByte(pos, 0xFF));
1102
1103 // Also save expanded messages to main ROM if any are loaded
1104 if (!expanded_messages_.empty()) {
1105 auto status = SaveExpandedMessages();
1106 if (!status.ok()) {
1107 std::copy(backup.begin(), backup.end(), rom()->mutable_data());
1108 return status;
1109 }
1110 }
1111
1112 return absl::OkStatus();
1113}
1114
1116 if (expanded_messages_.empty()) {
1117 return absl::OkStatus();
1118 }
1119
1120 if (!rom_ || !rom_->is_loaded()) {
1121 return absl::FailedPreconditionError("ROM not loaded");
1122 }
1123
1124 // Collect all expanded message text strings (mirrors CLI message-write path)
1125 std::vector<std::string> all_texts;
1126 all_texts.reserve(expanded_messages_.size());
1127 for (const auto& msg : expanded_messages_) {
1128 all_texts.push_back(msg.RawString);
1129 }
1130
1131 // Write to main ROM buffer at the expanded text data region
1134 GetExpandedTextDataEnd(), all_texts));
1135
1136 // Recalculate addresses after sequential write
1137 int pos = GetExpandedTextDataStart();
1138 for (auto& msg : expanded_messages_) {
1139 msg.Address = pos;
1140 auto bytes = ParseMessageToData(msg.RawString);
1141 pos += static_cast<int>(bytes.size()) + 1; // +1 for 0x7F terminator
1142 }
1143
1144 return absl::OkStatus();
1145}
1146
1148 if (!rom_ || !rom_->is_loaded()) {
1149 return absl::FailedPreconditionError("ROM not loaded");
1150 }
1151
1154
1155 expanded_messages_.clear();
1158
1159 if (expanded_messages_.empty()) {
1160 return absl::NotFoundError(
1161 "No expanded messages found in ROM at expanded text region");
1162 }
1163
1164 // Parse the expanded messages and append to the unified list
1165 auto parsed_expanded =
1167 for (const auto& msg : expanded_messages_) {
1168 if (msg.ID >= 0 && msg.ID < static_cast<int>(parsed_expanded.size())) {
1169 parsed_messages_.push_back(parsed_expanded[msg.ID]);
1170 }
1171 }
1172
1173 expanded_message_path_ = "(ROM)";
1174 return absl::OkStatus();
1175}
1176
1178 if (expanded_messages_.empty())
1179 return 0;
1180 int total = 0;
1181 for (const auto& msg : expanded_messages_) {
1182 total += static_cast<int>(msg.Data.size()) + 1; // +1 for 0x7F
1183 }
1184 total += 1; // +1 for final 0xFF
1185 return total;
1186}
1187
1188absl::Status MessageEditor::Cut() {
1189 // Ensure that text is currently selected in the text box.
1190 if (!message_text_box_.text.empty()) {
1191 // Cut the selected text in the control and paste it into the Clipboard.
1193 }
1194 return absl::OkStatus();
1195}
1196
1197absl::Status MessageEditor::Paste() {
1198 // Determine if there is any text in the Clipboard to paste into the
1199 if (ImGui::GetClipboardText() != nullptr) {
1200 // Paste the text from the Clipboard into the text box.
1202 }
1203 return absl::OkStatus();
1204}
1205
1206absl::Status MessageEditor::Copy() {
1207 // Ensure that text is selected in the text box.
1209 // Copy the selected text to the Clipboard.
1211 }
1212 return absl::OkStatus();
1213}
1214
1216 if (pending_undo_before_.has_value()) {
1217 // If we're still editing the same message, keep the existing "before"
1218 // snapshot so the entire edit session becomes a single undo step.
1219 if (pending_undo_before_->message_index == current_message_index_ &&
1221 return;
1222 }
1224 }
1225
1226 // Capture current state as "before"
1227 int parsed_index = current_message_index_;
1230 }
1231 std::string text;
1232 if (parsed_index >= 0 &&
1233 parsed_index < static_cast<int>(parsed_messages_.size())) {
1234 text = parsed_messages_[parsed_index];
1235 }
1239}
1240
1242 if (!pending_undo_before_.has_value())
1243 return;
1244
1245 // The "after" snapshot must correspond to the same message as the pending
1246 // "before", even if the user navigated to a different message in the UI.
1247 const int message_index = pending_undo_before_->message_index;
1248 const bool is_expanded = pending_undo_before_->is_expanded;
1249
1250 MessageData after_message;
1251 if (is_expanded) {
1252 if (message_index < 0 ||
1253 message_index >= static_cast<int>(expanded_messages_.size())) {
1254 pending_undo_before_.reset();
1255 return;
1256 }
1257 after_message = expanded_messages_[message_index];
1258 } else {
1259 if (message_index < 0 ||
1260 message_index >= static_cast<int>(list_of_texts_.size())) {
1261 pending_undo_before_.reset();
1262 return;
1263 }
1264 after_message = list_of_texts_[message_index];
1265 }
1266
1267 int parsed_index = message_index;
1268 if (is_expanded) {
1269 parsed_index = expanded_message_base_id_ + message_index;
1270 }
1271 std::string text;
1272 if (parsed_index >= 0 &&
1273 parsed_index < static_cast<int>(parsed_messages_.size())) {
1274 text = parsed_messages_[parsed_index];
1275 }
1276 MessageSnapshot after{std::move(after_message), std::move(text),
1277 message_index, is_expanded};
1278
1279 undo_manager_.Push(std::make_unique<MessageEditAction>(
1280 std::move(*pending_undo_before_), std::move(after),
1281 [this](const MessageSnapshot& s) { ApplySnapshot(s); }));
1282 pending_undo_before_.reset();
1283}
1284
1286 current_message_ = snapshot.message;
1290
1291 int parsed_index = snapshot.message_index;
1292 if (snapshot.is_expanded) {
1293 parsed_index = expanded_message_base_id_ + snapshot.message_index;
1294 }
1295 if (parsed_index >= 0 &&
1296 parsed_index < static_cast<int>(parsed_messages_.size())) {
1297 parsed_messages_[parsed_index] = snapshot.parsed_text;
1298 }
1299
1300 if (snapshot.is_expanded) {
1301 if (snapshot.message_index >= 0 &&
1302 snapshot.message_index < static_cast<int>(expanded_messages_.size())) {
1303 expanded_messages_[snapshot.message_index] = snapshot.message;
1304 }
1305 } else {
1306 if (snapshot.message_index >= 0 &&
1307 snapshot.message_index < static_cast<int>(list_of_texts_.size())) {
1308 list_of_texts_[snapshot.message_index] = snapshot.message;
1309 }
1310 }
1311
1313}
1314
1315absl::Status MessageEditor::Undo() {
1317 return undo_manager_.Undo();
1318}
1319
1320absl::Status MessageEditor::Redo() {
1321 return undo_manager_.Redo();
1322}
1323
1325 // Determine if any text is selected in the TextBox control.
1327 // clear all of the text in the textbox.
1329 }
1330}
1331
1333 // Determine if any text is selected in the TextBox control.
1335 // Select all text in the text box.
1337
1338 // Move the cursor to the text box.
1340 }
1341}
1342
1343absl::Status MessageEditor::Find() {
1344 if (ImGui::Begin("Find & Replace", nullptr,
1345 ImGuiWindowFlags_AlwaysAutoResize)) {
1346 static char find_text[256] = "";
1347 static char replace_text[256] = "";
1348 ImGui::InputText("Search", find_text, IM_ARRAYSIZE(find_text));
1349 ImGui::InputText("Replace with", replace_text, IM_ARRAYSIZE(replace_text));
1350
1351 if (ImGui::Button("Find Next")) {
1352 search_text_ = find_text;
1353 replace_status_.clear();
1354 }
1355
1356 ImGui::SameLine();
1357 if (ImGui::Button("Find All")) {
1358 search_text_ = find_text;
1359 replace_status_.clear();
1360 }
1361
1362 ImGui::SameLine();
1363 if (ImGui::Button("Replace")) {
1364 search_text_ = find_text;
1365 replace_text_ = replace_text;
1366 int count = ReplaceCurrentMatch();
1367 if (count > 0) {
1368 replace_status_ = "Replaced 1 occurrence";
1369 replace_status_error_ = false;
1370 } else {
1371 replace_status_ = "No match found in current message";
1372 replace_status_error_ = true;
1373 }
1374 }
1375
1376 ImGui::SameLine();
1377 if (ImGui::Button("Replace All")) {
1378 search_text_ = find_text;
1379 replace_text_ = replace_text;
1380 int count = ReplaceAllMatches();
1381 replace_status_ = absl::StrFormat("Replaced %d occurrence%s", count,
1382 count == 1 ? "" : "s");
1383 replace_status_error_ = (count == 0);
1384 }
1385
1386 ImGui::Checkbox("Case Sensitive", &case_sensitive_);
1387 ImGui::SameLine();
1388 ImGui::Checkbox("Match Whole Word", &match_whole_word_);
1389
1390 if (!replace_status_.empty()) {
1391 ImVec4 color =
1393 ImGui::TextColored(color, "%s", replace_status_.c_str());
1394 }
1395 }
1396 ImGui::End();
1397
1398 return absl::OkStatus();
1399}
1400
1402 if (search_text_.empty())
1403 return 0;
1404
1405 std::string& text = message_text_box_.text;
1406 std::string search = search_text_;
1407 std::string source = text;
1408
1409 if (!case_sensitive_) {
1410 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
1411 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
1412 }
1413
1414 size_t pos = source.find(search);
1415 if (pos == std::string::npos)
1416 return 0;
1417
1418 // Check whole word boundary if required
1419 if (match_whole_word_) {
1420 bool start_ok = (pos == 0 || !std::isalnum(source[pos - 1]));
1421 bool end_ok = (pos + search.size() >= source.size() ||
1422 !std::isalnum(source[pos + search.size()]));
1423 if (!start_ok || !end_ok) {
1424 // Search for a whole-word match further in the string
1425 while (pos != std::string::npos) {
1426 start_ok = (pos == 0 || !std::isalnum(source[pos - 1]));
1427 end_ok = (pos + search.size() >= source.size() ||
1428 !std::isalnum(source[pos + search.size()]));
1429 if (start_ok && end_ok)
1430 break;
1431 pos = source.find(search, pos + 1);
1432 }
1433 if (pos == std::string::npos)
1434 return 0;
1435 }
1436 }
1437
1438 // Perform the replacement in the original (case-preserving) text
1439 text.replace(pos, search_text_.size(), replace_text_);
1442 return 1;
1443}
1444
1446 if (search_text_.empty())
1447 return 0;
1448
1449 int total_replacements = 0;
1450
1451 auto replace_in_text = [&](std::string& text) -> int {
1452 int count = 0;
1453 std::string search = search_text_;
1454
1455 if (!case_sensitive_) {
1456 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
1457 }
1458
1459 size_t pos = 0;
1460 while (pos < text.size()) {
1461 std::string source = text;
1462 if (!case_sensitive_) {
1463 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
1464 }
1465
1466 size_t found = source.find(search, pos);
1467 if (found == std::string::npos)
1468 break;
1469
1470 if (match_whole_word_) {
1471 bool start_ok = (found == 0 || !std::isalnum(source[found - 1]));
1472 bool end_ok = (found + search.size() >= source.size() ||
1473 !std::isalnum(source[found + search.size()]));
1474 if (!start_ok || !end_ok) {
1475 pos = found + 1;
1476 continue;
1477 }
1478 }
1479
1480 text.replace(found, search_text_.size(), replace_text_);
1481 pos = found + replace_text_.size();
1482 count++;
1483 }
1484 return count;
1485 };
1486
1487 // Replace in vanilla messages
1488 for (size_t i = 0; i < list_of_texts_.size(); ++i) {
1489 int parsed_idx = static_cast<int>(i);
1490 if (parsed_idx < 0 ||
1491 parsed_idx >= static_cast<int>(parsed_messages_.size())) {
1492 continue;
1493 }
1494
1495 std::string text = parsed_messages_[parsed_idx];
1496 int count = replace_in_text(text);
1497 if (count > 0) {
1498 // Save current state, apply replacement, push undo
1499 int prev_index = current_message_index_;
1500 bool prev_expanded = current_message_is_expanded_;
1501 MessageData prev_message = current_message_;
1502 std::string prev_textbox = message_text_box_.text;
1503
1505 current_message_index_ = static_cast<int>(i);
1507 message_text_box_.text = text;
1510
1511 total_replacements += count;
1512
1513 // Restore previous editing context
1514 current_message_ = prev_message;
1515 current_message_index_ = prev_index;
1516 current_message_is_expanded_ = prev_expanded;
1517 message_text_box_.text = prev_textbox;
1518 }
1519 }
1520
1521 // Replace in expanded messages
1522 for (size_t i = 0; i < expanded_messages_.size(); ++i) {
1523 int parsed_idx = expanded_message_base_id_ + static_cast<int>(i);
1524 if (parsed_idx < 0 ||
1525 parsed_idx >= static_cast<int>(parsed_messages_.size())) {
1526 continue;
1527 }
1528
1529 std::string text = parsed_messages_[parsed_idx];
1530 int count = replace_in_text(text);
1531 if (count > 0) {
1532 int prev_index = current_message_index_;
1533 bool prev_expanded = current_message_is_expanded_;
1534 MessageData prev_message = current_message_;
1535 std::string prev_textbox = message_text_box_.text;
1536
1538 current_message_index_ = static_cast<int>(i);
1540 message_text_box_.text = text;
1543
1544 total_replacements += count;
1545
1546 // Restore previous editing context
1547 current_message_ = prev_message;
1548 current_message_index_ = prev_index;
1549 current_message_is_expanded_ = prev_expanded;
1550 message_text_box_.text = prev_textbox;
1551 }
1552 }
1553
1554 // Refresh the current message's text box from updated data
1555 int current_parsed_idx = current_message_index_;
1558 }
1559 if (current_parsed_idx >= 0 &&
1560 current_parsed_idx < static_cast<int>(parsed_messages_.size())) {
1561 message_text_box_.text = parsed_messages_[current_parsed_idx];
1562 }
1563
1564 // Refresh current_message_ to reflect replacements
1566 if (current_message_index_ >= 0 &&
1567 current_message_index_ < static_cast<int>(expanded_messages_.size())) {
1569 }
1570 } else {
1571 if (current_message_index_ >= 0 &&
1572 current_message_index_ < static_cast<int>(list_of_texts_.size())) {
1574 }
1575 }
1576
1577 return total_replacements;
1578}
1579
1580} // namespace editor
1581} // namespace yaze
auto begin()
Definition rom.h:141
auto mutable_data()
Definition rom.h:140
const auto & vector() const
Definition rom.h:143
auto data() const
Definition rom.h:139
auto size() const
Definition rom.h:138
bool is_loaded() const
Definition rom.h:132
const MessageLayout & message_layout() const
bool loaded() const
Check if the manifest has been loaded.
virtual void SetGameData(zelda3::GameData *game_data)
Definition editor.h:250
UndoManager undo_manager_
Definition editor.h:317
zelda3::GameData * game_data() const
Definition editor.h:307
EditorDependencies dependencies_
Definition editor.h:316
std::vector< std::string > parsed_messages_
absl::Status Copy() override
std::vector< MessageData > expanded_messages_
absl::Status Find() override
absl::Status Update() override
absl::Status LoadExpandedMessagesFromRom()
void UpdateCurrentMessageFromText(const std::string &text)
absl::Status Paste() override
void ApplySnapshot(const MessageSnapshot &snapshot)
void RefreshFontAtlasBitmap(const std::vector< uint8_t > &font_data)
absl::Status Undo() override
absl::Status Load() override
std::array< uint8_t, 0x4000 > raw_font_gfx_data_
gfx::SnesPalette BuildFallbackFontPalette() const
absl::Status Cut() override
std::optional< MessageSnapshot > pending_undo_before_
std::vector< MessageData > list_of_texts_
bool OpenMessageById(int display_id)
gfx::SnesPalette font_preview_colors_
absl::Status Redo() override
void ImportMessageBundleFromFile(const std::string &path)
absl::Status Save() override
void SetGameData(zelda3::GameData *game_data) override
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const SnesPalette & palette() const
Definition bitmap.h:368
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
TextureHandle texture() const
Definition bitmap.h:380
bool is_active() const
Definition bitmap.h:384
SnesPalette * mutable_palette()
Definition bitmap.h:369
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:853
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
SDL_Surface * surface() const
Definition bitmap.h:379
RAII timer for automatic timing management.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1157
void DrawContextMenu()
Definition canvas.cc:684
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1093
auto canvas_size() const
Definition canvas.h:451
auto zero_point() const
Definition canvas.h:443
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:590
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1480
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...
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
std::string DisplayTextOverflowError(int pos, bool bank)
constexpr int kCharactersWidth
int GetExpandedTextDataStart()
constexpr uint8_t kScrollVertical
std::optional< ResolvedMessageId > ResolveMessageDisplayId(int display_id, int vanilla_count, int expanded_base_id, int expanded_count)
absl::Status LoadExpandedMessages(std::string &expanded_message_path, std::vector< std::string > &parsed_messages, std::vector< MessageData > &expanded_messages, std::vector< DictionaryEntry > &dictionary)
constexpr uint8_t kLine1
constexpr int kTextData
std::string MessageBankToString(MessageBank bank)
constexpr int kCurrentMessageWidth
constexpr int kTextData2
constexpr int kCurrentMessageHeight
absl::Status WriteExpandedTextData(Rom *rom, int start, int end, const std::vector< std::string > &messages)
constexpr uint8_t kBlockTerminator
constexpr uint8_t kLine2
constexpr int kGfxFont
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
constexpr int kFontGfxMessageSize
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
constexpr uint8_t kMessageTerminator
constexpr int kFontGfxMessageDepth
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos)
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
std::vector< uint8_t > ParseMessageToData(std::string str)
absl::Status ExportMessagesToJson(const std::string &path, const std::vector< MessageData > &messages)
constexpr uint8_t kWidthArraySize
absl::Status ExportMessageBundleToJson(const std::string &path, const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr ImGuiTableFlags kMessageTableFlags
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
constexpr uint8_t kLine3
constexpr int kTextDataEnd
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:132
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1591
void BeginPadding(int i)
Definition style.cc:274
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:48
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1568
void EndNoPadding()
Definition style.cc:286
void MemoryEditorPopup(const std::string &label, std::span< uint8_t > memory)
Definition input.cc:699
void EndPadding()
Definition style.cc:278
void BeginNoPadding()
Definition style.cc:282
ImVec4 GetErrorColor()
Definition ui_helpers.cc:58
ImVec4 GetWarningColor()
Definition ui_helpers.cc:53
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
Definition color.cc:238
ImVec4 GetInfoColor()
Definition ui_helpers.cc:63
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:380
std::string HexWord(uint16_t word, HexStringParams params)
Definition hex.cc:41
std::string HexLong(uint32_t dword, HexStringParams params)
Definition hex.cc:52
absl::StatusOr< gfx::Bitmap > LoadFontGraphics(const Rom &rom)
Loads font graphics from ROM.
Definition game_data.cc:605
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
project::YazeProject * project
Definition editor.h:168
WorkspaceWindowManager * window_manager
Definition editor.h:176
std::vector< uint8_t > Data
std::vector< uint8_t > current_preview_data_
void DrawMessagePreview(const MessageData &message)
std::array< uint8_t, kWidthArraySize > width_array
std::vector< uint8_t > font_gfx16_data_2_
std::vector< uint8_t > font_gfx16_data_
std::vector< int > scroll_marker_lines
std::vector< DictionaryEntry > all_dictionaries_
auto palette(int i) const
void SelectAll()
Definition style.h:103
std::string text
Definition style.h:58
int selection_length
Definition style.h:63
core::HackManifest hack_manifest
Definition project.h:204
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91