7#include <unordered_map>
10#include "absl/status/status.h"
11#include "absl/strings/match.h"
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_format.h"
28#include "imgui/misc/cpp/imgui_stdlib.h"
42 std::string bankSTR = bank ?
"1st" :
"2nd";
44 bank ? absl::StrFormat(
"%X4", pos & 0xFFFF)
45 : absl::StrFormat(
"%X4", (pos -
kTextData2) & 0xFFFF);
46 std::string message = absl::StrFormat(
47 "There is too much text data in the %s block to save.\n"
48 "Available: %X4 | Used: %s",
49 bankSTR, space, posSTR);
54using ImGui::BeginChild;
55using ImGui::BeginTable;
59using ImGui::InputTextMultiline;
63using ImGui::Separator;
64using ImGui::TableHeadersRow;
65using ImGui::TableNextColumn;
66using ImGui::TableSetupColumn;
68using ImGui::TextWrapped;
71 ImGuiTableFlags_Borders |
72 ImGuiTableFlags_Resizable;
89 window_manager->RegisterWindowContent(
91 window_manager->RegisterWindowContent(
93 window_manager->RegisterWindowContent(
94 std::make_unique<FontAtlasPanel>([
this]() {
98 window_manager->RegisterWindowContent(
99 std::make_unique<DictionaryPanel>([
this]() {
106 window_manager->OpenWindow(session_id,
"message.message_list");
114 LOG_INFO(
"MessageEditor",
"Loaded %zu messages from ROM",
138 LOG_ERROR(
"MessageEditor",
"No messages found in ROM!");
149 const int vanilla_count =
static_cast<int>(
list_of_texts_.size());
154 expanded_base_id, expanded_count);
158 if (!resolved.has_value() && expanded_count == 0 &&
162 const size_t rom_size =
rom_->
size();
163 if (start >= 0 && end >= start &&
static_cast<size_t>(end) < rom_size) {
167 "OpenMessageById: expanded load skipped/failed: %s",
168 std::string(status.message()).c_str());
172 expanded_base_id, expanded_count);
175 "OpenMessageById: expanded region out of bounds (0x%X-0x%X, "
177 start, end, rom_size);
181 if (!resolved.has_value()) {
188 "message.message_list");
190 "message.message_editor");
193 if (!resolved->is_expanded) {
194 const int idx = resolved->index;
195 if (idx < 0 || idx >= vanilla_count) {
204 const int parsed_idx = resolved->display_id;
205 if (parsed_idx >= 0 &&
219 const int idx = resolved->index;
220 if (idx < 0 || idx >= expanded_count) {
229 const int parsed_idx = resolved->display_id;
230 if (parsed_idx >= 0 &&
247 if (layout.first_expanded_id != 0) {
248 base_id =
static_cast<int>(layout.first_expanded_id);
254 base_id = std::max(base_id,
static_cast<int>(
list_of_texts_.size()));
269 std::vector<gfx::SnesColor> colors;
271 for (
int i = 0; i < 16; ++i) {
272 const float value =
static_cast<float>(i) / 15.0f;
273 colors.emplace_back(ImVec4(value, value, value, 1.0f));
276 if (!colors.empty()) {
277 colors[0].set_transparent(
true);
285 if (!
rom() || !
rom()->is_loaded()) {
286 LOG_WARN(
"MessageEditor",
"ROM not loaded - skipping font graphics load");
292 const size_t rom_size =
rom()->
size();
293 if (rom_size >
static_cast<size_t>(
kGfxFont)) {
295 rom_size -
static_cast<size_t>(
kGfxFont));
300 "Font graphics truncated (ROM size %zu, read %zu bytes)",
301 rom_size, available);
305 "ROM size %zu too small for font graphics offset 0x%X", rom_size,
313 if (load_font.ok()) {
316 const std::string error_message(load_font.status().message());
317 LOG_WARN(
"MessageEditor",
"LoadFontGraphics failed: %s",
318 error_message.c_str());
329 const std::vector<uint8_t>& font_data) {
330 if (font_data.empty()) {
331 LOG_WARN(
"MessageEditor",
"Font graphics data missing - atlas stays empty");
336 const size_t row_count = (font_data.size() + atlas_width - 1) / atlas_width;
337 const int atlas_height =
static_cast<int>(std::max<size_t>(1, row_count));
339 const size_t expected_size =
static_cast<size_t>(atlas_width) * atlas_height;
340 std::vector<uint8_t> padded(font_data.begin(), font_data.end());
341 if (padded.size() < expected_size) {
342 padded.resize(expected_size, 0);
343 }
else if (padded.size() > expected_size) {
344 padded.resize(expected_size);
358 return absl::OkStatus();
365 return absl::OkStatus();
381 if (!bitmap.is_active()) {
384 const auto command = bitmap.texture()
420 if (BeginChild(
"##MessagesList", ImVec2(0, 0),
true,
421 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
423 if (ImGui::Button(tr(
"Import Bundle"))) {
430 if (ImGui::Button(tr(
"Export Bundle"))) {
437 absl::StrFormat(
"Export failed: %s", status.message());
452 TableSetupColumn(
"ID", ImGuiTableColumnFlags_WidthFixed, 50);
453 TableSetupColumn(
"Type", ImGuiTableColumnFlags_WidthFixed, 80);
454 TableSetupColumn(
"Contents", ImGuiTableColumnFlags_WidthStretch);
455 TableSetupColumn(
"Address", ImGuiTableColumnFlags_WidthFixed, 100);
460 const int vanilla_count =
static_cast<int>(
list_of_texts_.size());
462 const int total_rows = vanilla_count + expanded_count;
465 ImGuiListClipper clipper;
466 clipper.Begin(total_rows);
468 while (clipper.Step()) {
469 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
470 if (row < vanilla_count) {
493 int expanded_idx = row - vanilla_count;
495 const int display_id =
497 const char* display_text =
"Missing text";
498 if (display_id >= 0 &&
516 TextWrapped(
"%s", display_text);
519 TextWrapped(
"%s",
util::HexLong(expanded_message.Address).c_str());
533 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
536 if (ImGui::IsItemDeactivatedAfterEdit()) {
542 ImGui::BulletText(
"%s", error.c_str());
548 ImGui::BulletText(
"%s", warning.c_str());
552 if (!line_warnings.empty()) {
554 for (
const auto& warning : line_warnings) {
555 ImGui::BulletText(
"%s", warning.c_str());
561 ImGui::BeginChild(
"##MessagePreview", ImVec2(0, 0),
true);
563 Text(tr(
"Message Preview"));
564 if (Button(tr(
"View Palette"))) {
565 ImGui::OpenPopup(
"Palette");
567 if (ImGui::BeginPopup(
"Palette")) {
572 BeginChild(
"CurrentGfxFont", ImVec2(348, 0),
true,
573 ImGuiWindowFlags_NoScrollWithMouse);
581 if (ImGui::IsWindowHovered()) {
582 float wheel = ImGui::GetIO().MouseWheel;
585 }
else if (wheel < 0 &&
593 const float dest_width = std::max(0.0f, preview_canvas_size.x - 8.0f);
594 const float dest_height = std::max(0.0f, preview_canvas_size.y - 8.0f);
595 float src_height = 0.0f;
596 if (dest_width > 0.0f && dest_height > 0.0f) {
597 const float src_width =
603 ImVec2(dest_width, dest_height),
605 ImVec2(src_width, src_height)
611 ImDrawList* overlay_draw_list = ImGui::GetWindowDrawList();
614 float line_height = 16.0f;
617 float scale_y = 1.0f;
618 if (dest_height > 0.0f && src_height > 0.0f) {
619 scale_y = dest_height / src_height;
623 float y = canvas_p0.y + src_y * scale_y;
624 if (y >= canvas_p0.y && y <= canvas_p0.y + canvas_sz.y) {
625 overlay_draw_list->AddLine(ImVec2(canvas_p0.x, y),
626 ImVec2(canvas_p0.x + canvas_sz.x, y),
627 IM_COL32(100, 180, 255, 180), 1.5f);
628 overlay_draw_list->AddText(ImVec2(canvas_p0.x + canvas_sz.x + 4, y - 6),
629 IM_COL32(100, 180, 255, 200),
"[V]");
639 if (ImGui::CollapsingHeader(tr(
"Message Structure"),
640 ImGuiTreeNodeFlags_DefaultOpen)) {
643 int scroll_count = 0;
644 int current_line_chars = 0;
652 tr(
" [V] Scroll at byte %zu (line %d, %d chars)"),
653 i, line_num, current_line_chars);
654 current_line_chars = 0;
656 }
else if (
byte ==
kLine1) {
657 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
658 tr(
" [1] Line 1 at byte %zu"), i);
659 current_line_chars = 0;
661 }
else if (
byte ==
kLine2) {
662 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
663 tr(
" [2] Line 2 at byte %zu"), i);
664 current_line_chars = 0;
666 }
else if (
byte ==
kLine3) {
667 ImGui::TextColored(ImVec4(0.7f, 0.85f, 0.5f, 1.0f),
668 tr(
" [3] Line 3 at byte %zu"), i);
669 current_line_chars = 0;
671 }
else if (
byte < 100) {
672 current_line_chars++;
676 if (scroll_count == 0) {
677 ImGui::TextDisabled(tr(
"No scroll breaks in this message"));
679 ImGui::Text(tr(
"Total scroll breaks: %d"), scroll_count);
684 ImGui::TextDisabled(tr(
"Line width budget (max ~170px):"));
685 int estimated_line_width = current_line_chars * 8;
686 float width_ratio =
static_cast<float>(estimated_line_width) / 170.0f;
690 ImGui::TextColored(width_color, tr(
"Last line: ~%dpx / 170px (%d chars)"),
691 estimated_line_width, current_line_chars);
707 ImGui::BeginChild(
"##ExpandedMessageSettings", ImVec2(0, 130),
true,
708 ImGuiWindowFlags_AlwaysVerticalScrollbar);
709 ImGui::Text(tr(
"Expanded Messages"));
711 if (ImGui::Button(tr(
"Load from ROM"))) {
714 LOG_WARN(
"MessageEditor",
"Load from ROM: %s",
715 std::string(status.message()).c_str());
719 if (ImGui::Button(tr(
"Load from File"))) {
726 std::vector<std::string> parsed_expanded;
732 popup_manager->Show(
"Error");
736 parsed_expanded.end());
749 int remaining = capacity - used;
750 float usage_ratio =
static_cast<float>(used) /
static_cast<float>(capacity);
752 ImVec4 capacity_color;
753 if (usage_ratio < 0.75f) {
755 }
else if (usage_ratio < 0.90f) {
760 ImGui::TextColored(capacity_color, tr(
"Bank: %d / %d bytes (%d free)"),
761 used, capacity, remaining);
763 if (ImGui::Button(tr(
"Add New Message"))) {
771 if (display_id >= 0 &&
778 if (ImGui::Button(tr(
"Export to JSON"))) {
790 ImGui::BeginChild(
"##TextCommands",
791 ImVec2(0, ImGui::GetContentRegionAvail().y / 2),
true,
792 ImGuiWindowFlags_AlwaysVerticalScrollbar);
793 static uint8_t command_parameter = 0;
795 for (
const auto& text_element : TextCommands) {
796 if (Button(text_element.GenericToken.c_str())) {
798 text_element.GetParamToken(command_parameter));
802 TextWrapped(
"%s", text_element.Description.c_str());
809 ImGui::BeginChild(
"##SpecialChars",
810 ImVec2(0, ImGui::GetContentRegionAvail().y / 2),
true,
811 ImGuiWindowFlags_AlwaysVerticalScrollbar);
812 for (
const auto& text_element : SpecialChars) {
813 if (Button(text_element.GenericToken.c_str())) {
818 TextWrapped(
"%s", text_element.Description.c_str());
825 if (ImGui::BeginChild(
"##DictionaryChild",
826 ImVec2(0, ImGui::GetContentRegionAvail().y),
true,
827 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
829 TableSetupColumn(
"ID");
830 TableSetupColumn(
"Contents");
834 const int dict_count =
836 ImGuiListClipper clipper;
837 clipper.Begin(dict_count);
839 while (clipper.Step()) {
840 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
845 Text(
"%s", dictionary.Contents.c_str());
870 if (!parse_result.ok()) {
874 std::string raw_text = text;
875 raw_text.erase(std::remove(raw_text.begin(), raw_text.end(),
'\n'),
887 if (parsed_index >= 0) {
914 if (!entries_or.ok()) {
916 absl::StrFormat(
"Import failed: %s", entries_or.status().message());
924 int duplicate_errors = 0;
925 int parse_error_entries = 0;
926 int vanilla_updated = 0;
927 int expanded_updated = 0;
928 int expanded_created = 0;
929 bool expanded_modified =
false;
930 std::vector<std::string> issue_samples;
932 auto add_issue_sample = [&issue_samples](
const std::string& issue) {
933 constexpr size_t kMaxIssueSamples = 4;
934 if (issue_samples.size() < kMaxIssueSamples) {
935 issue_samples.push_back(issue);
943 std::unordered_map<std::string, int> seen_entries;
945 auto entries = entries_or.value();
946 for (
const auto& entry : entries) {
947 const std::string entry_key = make_entry_key(entry);
948 if (seen_entries.find(entry_key) != seen_entries.end()) {
951 add_issue_sample(absl::StrFormat(
"Duplicate entry for %s", entry_key));
954 seen_entries.emplace(entry_key, 1);
958 warnings +=
static_cast<int>(parse_result.warnings.size());
959 warnings +=
static_cast<int>(line_warnings.size());
961 if (!parse_result.ok()) {
963 parse_error_entries++;
964 if (!parse_result.errors.empty()) {
965 add_issue_sample(absl::StrFormat(
"Parse error for %s: %s", entry_key,
966 parse_result.errors.front()));
968 add_issue_sample(absl::StrFormat(
"Parse error for %s", entry_key));
974 if (entry.id < 0 || entry.id >=
static_cast<int>(
list_of_texts_.size())) {
977 absl::StrFormat(
"Vanilla ID out of range: %d", entry.id));
981 message.RawString = entry.text;
982 message.ContentsParsed = entry.text;
983 message.Data = parse_result.bytes;
984 message.DataParsed = parse_result.bytes;
995 absl::StrFormat(
"Expanded ID out of range: %d", entry.id));
1000 const int target_size = entry.id + 1;
1002 for (
int i = old_size; i < target_size; ++i) {
1005 expanded_created += target_size - old_size;
1008 message.RawString = entry.text;
1009 message.ContentsParsed = entry.text;
1010 message.Data = parse_result.bytes;
1011 message.DataParsed = parse_result.bytes;
1013 if (parsed_index >= 0) {
1019 expanded_modified =
true;
1025 if (expanded_modified) {
1028 message.Address = pos;
1029 pos +=
static_cast<int>(message.Data.size()) + 1;
1037 if (current_display_id >= 0) {
1043 "Import finished with %d errors (%d applied: vanilla %d updated, "
1044 "expanded %d updated/%d created; %d warnings, %d duplicates, %d "
1046 errors, applied, vanilla_updated, expanded_updated, expanded_created,
1047 warnings, duplicate_errors, parse_error_entries);
1048 if (!issue_samples.empty()) {
1055 "Imported %d messages (vanilla %d updated, expanded %d updated/%d "
1056 "created, %d warnings).",
1057 applied, vanilla_updated, expanded_updated, expanded_created, warnings);
1059 if (applied > 0 &&
rom_) {
1062 if (vanilla_updated > 0) {
1065 if (expanded_modified) {
1076 LOG_WARN(
"MessageEditor",
"Preview data is empty, skipping bitmap update");
1092 LOG_ERROR(
"MessageEditor",
"Bitmap surface is null after set_data()");
1104 "Updated message preview bitmap (size: %zu) and queued texture update",
1116 "Created message preview bitmap (%dx%d) with 8-bit depth and "
1117 "queued texture creation",
1124 return absl::FailedPreconditionError(
"ROM not loaded");
1127 return absl::FailedPreconditionError(absl::StrFormat(
1137 bool include_font_widths,
bool include_vanilla_messages,
1138 bool include_expanded_messages)
const {
1140 return absl::FailedPreconditionError(
"ROM not loaded");
1154 std::vector<uint8_t> primary;
1155 std::vector<uint8_t> secondary;
1158 primary.reserve(kPrimaryCapacity);
1159 secondary.reserve(kSecondaryCapacity);
1161 bool in_second_bank =
false;
1162 auto append_byte = [&](uint8_t value,
bool is_bank_switch) -> absl::Status {
1163 auto&
destination = in_second_bank ? secondary : primary;
1164 const size_t capacity =
1165 in_second_bank ? kSecondaryCapacity : kPrimaryCapacity;
1169 return absl::ResourceExhaustedError(
1170 DisplayTextOverflowError(overflow_pos, !in_second_bank));
1173 if (!in_second_bank && is_bank_switch) {
1174 in_second_bank =
true;
1176 return absl::OkStatus();
1180 bool next_byte_is_command_argument =
false;
1181 for (uint8_t value : message.Data) {
1182 const bool is_command_argument = next_byte_is_command_argument;
1183 next_byte_is_command_argument =
false;
1186 if (!is_command_argument) {
1188 next_byte_is_command_argument =
1189 command.has_value() && command->HasArgument;
1198 if (!primary.empty()) {
1202 if (!secondary.empty()) {
1212 if (start < 0 || end < start) {
1213 return absl::InvalidArgumentError(
"Invalid expanded message region");
1215 if (
static_cast<uint64_t
>(start) >=
rom_->
size() ||
1216 static_cast<uint64_t
>(end) >=
rom_->
size()) {
1217 return absl::OutOfRangeError(
1218 "Expanded message region is outside the ROM");
1221 const int64_t capacity =
static_cast<int64_t
>(end) - start + 1;
1222 std::vector<uint8_t> bytes;
1223 bytes.reserve(
static_cast<size_t>(capacity));
1230 return absl::InvalidArgumentError(
1231 absl::StrFormat(
"Expanded message %d is invalid: %s",
1232 static_cast<int>(index), parsed.errors.front()));
1234 if (message.RawString.find(
"[BANK]") != std::string::npos) {
1235 return absl::InvalidArgumentError(absl::StrFormat(
1236 "Expanded message %d contains [BANK], which is only valid in the "
1237 "vanilla message stream",
1238 static_cast<int>(index)));
1241 const int64_t needed =
static_cast<int64_t
>(parsed.bytes.size()) + 1;
1242 if (
static_cast<int64_t
>(bytes.size()) + needed + 1 > capacity) {
1243 return absl::ResourceExhaustedError(absl::StrFormat(
1244 "Expanded message data exceeds bank boundary "
1245 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
1246 static_cast<int>(index),
static_cast<int>(bytes.size()),
1247 static_cast<int>(needed),
static_cast<int>(capacity), end));
1251 static_cast<int>(bytes.size()));
1252 bytes.insert(bytes.end(), parsed.bytes.begin(), parsed.bytes.end());
1255 bytes.push_back(0xFF);
1259 static_cast<uint32_t
>(start), std::move(bytes)});
1262 for (
const auto& write : plan.
writes) {
1263 const uint64_t end =
1264 static_cast<uint64_t
>(write.start) + write.bytes.size();
1265 if (end >
rom_->
size() || end > std::numeric_limits<uint32_t>::max()) {
1266 return absl::OutOfRangeError(absl::StrFormat(
1267 "Message save range [0x%06X, 0x%06llX) is outside the ROM",
1268 write.start,
static_cast<unsigned long long>(end)));
1278 return absl::OkStatus();
1281 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1282 ranges.reserve(plan.
writes.size());
1283 for (
const auto& write : plan.
writes) {
1284 ranges.emplace_back(write.start, write.end());
1288 if (yaze_project.rom_metadata.write_policy ==
1291 absl::StrContains(yaze_project.hack_manifest.hack_name(),
1292 "Oracle of Secrets")) {
1293 return absl::PermissionDeniedError(
1294 "Oracle of Secrets expanded messages are owned by ASM. No message "
1295 "bytes were written; keep the draft in Yaze, edit Core/message.asm, "
1296 "rebuild Oracle of Secrets, and reopen the rebuilt ROM before "
1301 yaze_project.hack_manifest, yaze_project.rom_metadata.write_policy,
1307 if (plan.
writes.empty()) {
1308 return absl::OkStatus();
1312 for (
const auto& write : plan.
writes) {
1313 const char* label =
"MessageData";
1314 switch (write.domain) {
1316 label =
"MessageFontWidths";
1319 label =
"VanillaMessageBank";
1322 label =
"ExpandedMessageBank";
1330 for (
const auto& write : plan.
writes) {
1346 return absl::OkStatus();
1350 bool include_vanilla_messages,
1351 bool include_expanded_messages) {
1353 BuildSavePlan(include_font_widths, include_vanilla_messages,
1354 include_expanded_messages));
1358 return absl::OkStatus();
1369 return absl::FailedPreconditionError(
1370 "Message save transaction is already active");
1380 return absl::OkStatus();
1413 bool* dirty =
nullptr;
1414 bool* transaction_saved =
nullptr;
1431 *transaction_saved =
false;
1472 return absl::FailedPreconditionError(
"ROM not loaded");
1484 return absl::NotFoundError(
1485 "No expanded messages found in ROM at expanded text region");
1489 auto parsed_expanded =
1492 if (msg.ID >= 0 && msg.ID <
static_cast<int>(parsed_expanded.size())) {
1499 return absl::OkStatus();
1507 total +=
static_cast<int>(msg.Data.size()) + 1;
1519 return absl::OkStatus();
1524 if (ImGui::GetClipboardText() !=
nullptr) {
1528 return absl::OkStatus();
1537 return absl::OkStatus();
1557 if (parsed_index >= 0 &&
1577 if (message_index < 0 ||
1584 if (message_index < 0 ||
1592 int parsed_index = message_index;
1597 if (parsed_index >= 0 &&
1602 message_index, is_expanded};
1615 const auto diagnostics =
1624 if (parsed_index >= 0 &&
1678 if (ImGui::Begin(
"Find & Replace",
nullptr,
1679 ImGuiWindowFlags_AlwaysAutoResize)) {
1680 static char find_text[256] =
"";
1681 static char replace_text[256] =
"";
1682 ImGui::InputText(tr(
"Search"), find_text, IM_ARRAYSIZE(find_text));
1683 ImGui::InputText(tr(
"Replace with"), replace_text,
1684 IM_ARRAYSIZE(replace_text));
1686 if (ImGui::Button(tr(
"Find Next"))) {
1692 if (ImGui::Button(tr(
"Find All"))) {
1698 if (ImGui::Button(tr(
"Replace"))) {
1712 if (ImGui::Button(tr(
"Replace All"))) {
1718 count == 1 ?
"" :
"s");
1735 return absl::OkStatus();
1744 std::string source = text;
1747 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
1748 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
1751 size_t pos = source.find(search);
1752 if (pos == std::string::npos)
1757 bool start_ok = (pos == 0 || !std::isalnum(source[pos - 1]));
1758 bool end_ok = (pos + search.size() >= source.size() ||
1759 !std::isalnum(source[pos + search.size()]));
1760 if (!start_ok || !end_ok) {
1762 while (pos != std::string::npos) {
1763 start_ok = (pos == 0 || !std::isalnum(source[pos - 1]));
1764 end_ok = (pos + search.size() >= source.size() ||
1765 !std::isalnum(source[pos + search.size()]));
1766 if (start_ok && end_ok)
1768 pos = source.find(search, pos + 1);
1770 if (pos == std::string::npos)
1788 "Replace All blocked: resolve the current message parse errors first";
1793 auto replace_in_text = [&](std::string& text) ->
int {
1798 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
1802 while (pos < text.size()) {
1803 std::string source = text;
1805 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
1808 size_t found = source.find(search, pos);
1809 if (found == std::string::npos)
1813 bool start_ok = (found == 0 || !std::isalnum(source[found - 1]));
1814 bool end_ok = (found + search.size() >= source.size() ||
1815 !std::isalnum(source[found + search.size()]));
1816 if (!start_ok || !end_ok) {
1829 struct PlannedReplacement {
1835 std::vector<PlannedReplacement> plan;
1837 const auto plan_replacements = [&](
const std::vector<MessageData>& messages,
1838 bool is_expanded) ->
bool {
1839 for (
size_t i = 0; i < messages.size(); ++i) {
1840 const int parsed_index =
1842 if (parsed_index < 0 ||
1848 const int count = replace_in_text(text);
1855 (is_expanded && text.find(
"[BANK]") != std::string::npos)) {
1856 const std::string error =
1857 !parsed.ok() ? parsed.errors.front()
1858 :
"[BANK] is not valid in expanded messages";
1860 "Replace All aborted at %s message %d: %s",
1861 is_expanded ?
"expanded" :
"vanilla",
static_cast<int>(i), error);
1867 {
static_cast<int>(i), is_expanded, std::move(text), count});
1880 int total_replacements = 0;
1882 for (
const auto& replacement : plan) {
1891 total_replacements += replacement.count;
1902 if (current_parsed_idx >= 0 &&
1920 const auto current_diagnostics =
1925 return total_replacements;
void set_dirty(bool dirty)
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
const MessageLayout & message_layout() const
bool loaded() const
Check if the manifest has been loaded.
virtual void SetGameData(zelda3::GameData *game_data)
UndoManager undo_manager_
zelda3::GameData * game_data() const
EditorDependencies dependencies_
DirtyState transaction_dirty_snapshot_
std::vector< std::string > parsed_messages_
absl::Status Copy() override
bool current_message_is_expanded_
bool replace_status_error_
void CommitSaveTransaction() override
std::vector< MessageData > expanded_messages_
int CalculateExpandedBankUsage() const
absl::Status Find() override
void ResolveFontPalette()
void EnsureFontTexturesReady()
absl::Status SaveExpandedMessages()
DirtyState transaction_saved_domains_
void RollbackSaveTransaction() override
bool font_graphics_loaded_
absl::Status Update() override
void ClearSavedDomains(const DirtyState &saved)
void FinalizePendingUndo()
absl::Status LoadExpandedMessagesFromRom()
void RecordSavedDomains(const SavePlan &plan)
void DrawExpandedMessageSettings()
void UpdateCurrentMessageFromText(const std::string &text)
absl::Status Paste() override
void ApplySnapshot(const MessageSnapshot &snapshot)
bool save_transaction_active_
absl::StatusOr< SavePlan > BuildSavePlan(bool include_font_widths, bool include_vanilla_messages, bool include_expanded_messages) const
std::vector< int > transaction_expanded_address_snapshot_
void SyncCurrentExpandedMessageAddress()
void MarkDomainDirty(SaveDomain domain)
gui::Canvas current_font_gfx16_canvas_
gui::Canvas font_gfx_canvas_
void RefreshFontAtlasBitmap(const std::vector< uint8_t > &font_data)
std::vector< std::string > current_parse_errors_
absl::Status Undo() override
absl::Status Load() override
std::string replace_status_
std::vector< std::string > current_parse_warnings_
std::string message_bundle_status_
std::array< uint8_t, 0x4000 > raw_font_gfx_data_
gfx::SnesPalette BuildFallbackFontPalette() const
void Initialize() override
std::string replace_text_
absl::Status SaveDirtyDomains(bool include_font_widths, bool include_vanilla_messages, bool include_expanded_messages)
absl::Status Cut() override
void DrawMessagePreview()
int expanded_message_base_id_
bool message_bundle_status_error_
std::optional< MessageSnapshot > pending_undo_before_
absl::Status BeginSaveTransaction() override
std::vector< MessageData > list_of_texts_
bool OpenMessageById(int display_id)
gfx::SnesPalette font_preview_colors_
MessageData current_message_
MessagePreview message_preview_
gui::TextBox message_text_box_
void DrawCurrentMessage()
void DrawSpecialCharacters()
absl::Status Redo() override
int ResolveExpandedMessageBaseId() const
void ImportMessageBundleFromFile(const std::string &path)
gfx::Bitmap current_font_gfx16_bitmap_
absl::Status ValidateSavePlan(const SavePlan &plan) const
int ReplaceCurrentMatch()
int current_message_index_
absl::Status ApplySavePlan(const SavePlan &plan)
gfx::Bitmap font_gfx_bitmap_
std::string expanded_message_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)
Represents a bitmap image optimized for SNES ROM hacking.
const SnesPalette & palette() const
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
TextureHandle texture() const
SnesPalette * mutable_palette()
void set_data(const std::vector< uint8_t > &data)
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
SDL_Surface * surface() const
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)
bool DrawTileSelector(int size, int size_y=0)
CanvasConfig & GetConfig()
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
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,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define PRINT_IF_ERROR(expression)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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 ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
absl::Status LoadExpandedMessages(std::string &expanded_message_path, std::vector< std::string > &parsed_messages, std::vector< MessageData > &expanded_messages, std::vector< DictionaryEntry > &dictionary)
std::string MessageBankToString(MessageBank bank)
constexpr int kCurrentMessageWidth
constexpr int kCurrentMessageHeight
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
constexpr int kFontGfxMessageSize
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos, bool allow_bank_switch)
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
constexpr uint8_t kMessageTerminator
constexpr int kFontGfxMessageDepth
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t kBankSwitchCommand
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)
std::optional< TextElement > FindMatchingCommand(uint8_t b)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
constexpr int kTextDataEnd
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void MemoryEditorPopup(const std::string &label, std::span< uint8_t > memory)
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
std::string HexWord(uint16_t word, HexStringParams params)
std::string HexLong(uint32_t dword, HexStringParams params)
absl::StatusOr< gfx::Bitmap > LoadFontGraphics(const Rom &rom)
Loads font graphics from ROM.
#define RETURN_IF_ERROR(expr)
Room transition destination.
project::YazeProject * project
ToastManager * toast_manager
WorkspaceWindowManager * window_manager
PopupManager * popup_manager
std::vector< uint8_t > Data
std::string ContentsParsed
std::vector< int > expanded_message_addresses
bool saves_vanilla_messages
bool saves_expanded_messages
std::vector< PlannedWrite > writes
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
core::HackManifest hack_manifest
gfx::PaletteGroupMap palette_groups