12#include <unordered_map>
15#include "absl/status/status.h"
16#include "absl/strings/match.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
40#include "imgui/imgui_internal.h"
41#include "imgui/misc/cpp/imgui_stdlib.h"
53using ImGui::BeginChild;
54using ImGui::BeginTable;
58using ImGui::InputTextMultiline;
62using ImGui::Separator;
63using ImGui::TableHeadersRow;
64using ImGui::TableNextColumn;
65using ImGui::TableSetupColumn;
67using ImGui::TextWrapped;
70 ImGuiTableFlags_Borders |
71 ImGuiTableFlags_Resizable;
89 window_manager->RegisterWindowContent(
91 window_manager->RegisterWindowContent(
93 window_manager->RegisterWindowContent(
94 std::make_unique<FontAtlasPanel>([
this]() {
98 window_manager->RegisterWindowContent(
102 window_manager->OpenWindow(session_id,
"message.message_list");
109 const int message_read_limit =
static_cast<int>(
110 std::min(
static_cast<size_t>(
rom()->size()),
111 static_cast<size_t>(std::numeric_limits<int>::max())));
114 LOG_INFO(
"MessageEditor",
"Loaded %zu messages from ROM",
139 LOG_ERROR(
"MessageEditor",
"No messages found in ROM!");
150 const int vanilla_count =
static_cast<int>(
list_of_texts_.size());
155 expanded_base_id, expanded_count);
159 if (!resolved.has_value() && expanded_count == 0 &&
163 const int start = range.first;
164 const int end = range.second;
165 const size_t rom_size =
rom_->
size();
166 if (start >= 0 && end >= start &&
static_cast<size_t>(end) < rom_size) {
170 "OpenMessageById: expanded load skipped/failed: %s",
171 std::string(status.message()).c_str());
175 expanded_base_id, expanded_count);
178 "OpenMessageById: expanded region out of bounds (0x%X-0x%X, "
180 start, end, rom_size);
184 if (!resolved.has_value()) {
191 "message.message_list");
193 "message.message_editor");
196 if (!resolved->is_expanded) {
197 const int idx = resolved->index;
198 if (idx < 0 || idx >= vanilla_count) {
207 const int parsed_idx = resolved->display_id;
208 if (parsed_idx >= 0 &&
227 const int idx = resolved->index;
228 if (idx < 0 || idx >= expanded_count) {
237 const int parsed_idx = resolved->display_id;
238 if (parsed_idx >= 0 &&
260 if (layout.first_expanded_id != 0) {
261 base_id =
static_cast<int>(layout.first_expanded_id);
267 base_id = std::max(base_id,
static_cast<int>(
list_of_texts_.size()));
274 return absl::OkStatus();
280 return absl::OkStatus();
285 "Expanded source unavailable: %s. Vanilla dialogue remains available.",
291 return absl::OkStatus();
298 return absl::OkStatus();
309 int* cursor =
nullptr;
310 int* select_start =
nullptr;
311 int* select_end =
nullptr;
316 if (state ==
nullptr) {
319 if (state->select_start !=
nullptr && state->select_end !=
nullptr &&
320 *state->select_start >= 0) {
322 const int end = std::max(start, *state->select_end);
323 data->CursorPos = std::min(end, data->BufTextLen);
324 data->SelectionStart = std::min(start, data->BufTextLen);
325 data->SelectionEnd = data->CursorPos;
326 *state->select_start = -1;
327 *state->select_end = -1;
329 if (state->cursor !=
nullptr) {
330 *state->cursor = data->CursorPos;
336 ImGuiContext* context = ImGui::GetCurrentContext();
337 if (context ==
nullptr || context->NavWindow ==
nullptr) {
340 ImGuiWindow* root = context->NavWindow->RootWindow !=
nullptr
341 ? context->NavWindow->RootWindow
342 : context->NavWindow;
343 return root->Name !=
nullptr &&
344 std::strstr(root->Name,
"message.message") !=
nullptr;
350 if (!MessageSurfaceFocused()) {
353 const ImGuiIO& io = ImGui::GetIO();
354 if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift &&
355 ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
358 if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift &&
359 ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
362 if (!io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F3,
false)) {
372 if (status_bar ==
nullptr) {
387 absl::StrFormat(
"%d/%d", shown, total));
407 if (display_id < 0 ||
415 bool apply_filter)
const {
416 std::vector<MessageRow> rows;
419 const auto consider = [&](
int display_id,
bool expanded, uint32_t address) {
426 rows.push_back(
MessageRow{display_id, expanded, address});
429 consider(message.ID,
false,
static_cast<uint32_t
>(message.Address));
433 static_cast<uint32_t
>(message.Address));
443 const int pos = std::clamp(
editor_cursor_, 0,
static_cast<int>(text.size()));
444 text.insert(
static_cast<size_t>(pos), token);
448 ImGui::ClearActiveID();
468 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
469 if (rows[i].display_id == current_id &&
476 std::clamp(current + delta, 0,
static_cast<int>(rows.size()) - 1);
477 if (next == current) {
503 const int end =
static_cast<int>(offset +
search_text_.size());
509 wrapped ?
" (wrapped)" :
"");
529 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
530 if (rows[i].display_id == current_id &&
537 const int count =
static_cast<int>(rows.size());
538 const size_t cursor =
static_cast<size_t>(std::clamp(
543 auto match_at = [&](
int row_index,
size_t bound,
544 bool from_start) -> std::optional<size_t> {
545 const std::string_view text =
551 const size_t before = from_start ? std::string::npos : bound;
557 match_at(start, forward ? forward_from : backward_before,
false)) {
560 for (
int step = 1; step < count; ++step) {
561 const int row_index =
562 forward ? (start + step) % count : (start - step + count) % count;
563 if (
auto found = match_at(row_index, 0,
true)) {
570 const bool can_wrap = forward ? forward_from > 0 :
true;
572 if (
auto found = match_at(start, 0,
true)) {
602 ImGui::ClearActiveID();
614 ImGui::PushID(
"FindReplace");
616 ImGui::SetKeyboardFocusHere();
620 std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f - 8.0f);
621 ImGui::SetNextItemWidth(half);
622 if (ImGui::InputTextWithHint(
"##find",
"Find in dialogue", &
search_text_)) {
626 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
627 ImGui::InputTextWithHint(
"##replace",
"Replace with", &
replace_text_);
629 if (ImGui::Button(tr(
"Find Next"))) {
633 if (ImGui::Button(tr(
"Previous"))) {
637 if (ImGui::Button(tr(
"Find All"))) {
646 matches, matches == 1 ?
"" :
"s");
651 "message.message_list");
656 if (ImGui::Button(tr(
"Replace"))) {
670 if (ImGui::Button(tr(
"Replace All"))) {
674 count == 1 ?
"" :
"s");
679 if (ImGui::SmallButton(tr(
"Close"))) {
684 if (ImGui::IsItemHovered()) {
685 ImGui::SetTooltip(
"Applies to Find Next, Previous, and Replace.");
689 if (ImGui::IsItemHovered()) {
690 ImGui::SetTooltip(
"Match only when both sides are not letters or digits.");
713 if (display_id < 0 ||
725 if (display_id < 0) {
741 const float label_h = ImGui::GetFrameHeightWithSpacing();
742 const float text_h = std::max(48.0f, height - label_h);
744 auto draw_editor = [&](
float editor_height) {
748 ImVec2(-1.0f, editor_height),
749 ImGuiInputTextFlags_CallbackAlways,
750 MessageInputCallback, &input_state)) {
753 if (ImGui::IsItemDeactivatedAfterEdit()) {
758 if (!show_reference) {
763 const bool side_by_side = ImGui::GetContentRegionAvail().x >= 640.0f;
765 ImGui::BeginTable(
"##DialogueColumns", 2, ImGuiTableFlags_Resizable,
766 ImVec2(0.0f, height))) {
767 ImGui::TableSetupColumn(tr(
"Loaded"), ImGuiTableColumnFlags_WidthStretch,
769 ImGui::TableSetupColumn(tr(
"Editing"), ImGuiTableColumnFlags_WidthStretch,
771 ImGui::TableNextRow();
772 ImGui::TableNextColumn();
773 ImGui::TextDisabled(tr(
"Loaded"));
775 ImVec2(-1.0f, text_h),
776 ImGuiInputTextFlags_ReadOnly);
777 ImGui::TableNextColumn();
778 ImGui::TextDisabled(tr(
"Editing"));
784 ImGui::TextDisabled(tr(
"Loaded"));
785 const float reference_h = std::clamp(height * 0.34f, 64.0f, 140.0f);
787 ImVec2(-1.0f, reference_h),
788 ImGuiInputTextFlags_ReadOnly);
789 ImGui::TextDisabled(tr(
"Editing"));
790 draw_editor(std::max(48.0f, height - reference_h - label_h * 2.0f));
808 return {fallback_start, fallback_end};
811 if (layout.data_start == 0 || layout.data_end < layout.data_start) {
812 return {fallback_start, fallback_end};
814 const int start =
static_cast<int>(
SnesToPc(layout.data_start));
815 const int end =
static_cast<int>(
SnesToPc(layout.data_end));
817 return {fallback_start, fallback_end};
825 "The draft changed after preview. Preview again before publishing.";
834absl::StatusOr<std::filesystem::path>
837 return absl::FailedPreconditionError(
838 "The project hack manifest does not define messages.source");
841 .
source->canonical_bundle_path;
842 const std::filesystem::path relative(configured);
843 if (relative.empty() || relative.is_absolute() || relative.has_root_name()) {
844 return absl::InvalidArgumentError(
845 "Canonical message bundle must be a project-relative path");
847 std::error_code error;
848 const std::filesystem::path descriptor =
851 return absl::FailedPreconditionError(
852 absl::StrFormat(
"Cannot resolve project descriptor %s: %s",
855 const std::filesystem::path root =
856 std::filesystem::canonical(descriptor.parent_path(), error);
858 return absl::FailedPreconditionError(
859 absl::StrFormat(
"Cannot resolve project root for %s: %s",
862 const std::filesystem::path joined = (root / relative).lexically_normal();
863 const std::filesystem::path from_root = joined.lexically_relative(root);
864 if (from_root.empty() || from_root.is_absolute() ||
865 *from_root.begin() ==
"..") {
866 return absl::InvalidArgumentError(
867 "Canonical message bundle must stay inside the project root");
873 const std::filesystem::path& path)
const {
874 std::ifstream input(path, std::ios::binary);
875 if (!input.is_open()) {
876 return absl::NotFoundError(absl::StrFormat(
877 "Cannot read canonical message bundle: %s", path.string()));
879 const std::string content((std::istreambuf_iterator<char>(input)),
880 std::istreambuf_iterator<char>());
881 if (!input.good() && !input.eof()) {
882 return absl::InternalError(absl::StrFormat(
883 "Cannot read canonical message bundle: %s", path.string()));
890 if (!canonical_or.ok() || path.empty()) {
893 std::error_code error;
894 const auto incoming = std::filesystem::weakly_canonical(path, error);
899 std::filesystem::weakly_canonical(canonical_or.value(), error);
900 return !error && incoming == target;
944 std::error_code error;
945 std::filesystem::path directory = std::filesystem::temp_directory_path(error);
947 directory = std::filesystem::current_path(error);
949 return (directory / absl::StrFormat(
"yaze-message-source-sync-%zu.json",
972 "The project hack manifest does not define messages.source";
978 "Load the canonical expanded bundle before previewing publication";
983 const auto canonical_sha_or = [&]() -> absl::StatusOr<std::string> {
986 return path_or.status();
990 if (!canonical_sha_or.ok()) {
997 "Canonical source changed on disk after it was loaded. Reload the "
998 "canonical bundle before preview. This draft was not published.";
1004 const auto write_status =
1006 if (!write_status.ok()) {
1008 "Could not write the preview bundle: %s", write_status.message());
1012 const auto draft_sha_or =
1014 if (!draft_sha_or.ok()) {
1022 if (!result_or.ok()) {
1034 "%s. %d update%s, %zu / %zu bytes. SHA %s",
1035 result.
changed ?
"Source would change" :
"Source already matches",
1061 const auto write_status =
1063 if (!write_status.ok()) {
1065 "Could not write the publication bundle: %s", write_status.message());
1069 const auto draft_sha_or =
1075 "The draft changed after preview. Preview again before publishing. "
1076 "Nothing was written.";
1082 const auto canonical_sha_or =
1083 canonical_path_or.ok()
1085 : absl::StatusOr<std::string>(canonical_path_or.status());
1086 if (!canonical_sha_or.ok() ||
1092 "Canonical source changed after preview. Reload the canonical bundle "
1093 "and preview again. This draft was not published.";
1099 options.
write =
true;
1105 if (!result_or.ok()) {
1114 result_or->wrote ?
"Published the canonical bundle and ASM include. "
1115 "Rebuild before the patched ROM shows this text."
1116 :
"Canonical source already matched; nothing written";
1119 absl::StrFormat(
"%s\n%s", result_or->canonical_bundle_path.string(),
1120 result_or->generated_asm_include_path.string());
1128 if (ImGui::Button(tr(
"Preview Publication"))) {
1132 (ImGui::GetItemFlags() & ImGuiItemFlags_Disabled) != 0;
1133 ImGui::EndDisabled();
1136 if (ImGui::Button(tr(
"Publish"))) {
1137 ImGui::OpenPopup(
"Publish message source###PublishMessageSource");
1140 (ImGui::GetItemFlags() & ImGuiItemFlags_Disabled) != 0;
1141 ImGui::EndDisabled();
1151 tr(
"Load a project whose hack manifest defines messages.source to "
1152 "publish expanded dialogue as a canonical bundle and ASM include."));
1156 const auto& source =
1158 ImGui::TextDisabled(
"%s", source.canonical_bundle_path.c_str());
1159 ImGui::TextDisabled(
"%s", source.generated_asm_include_path.c_str());
1162 tr(
"Preview checks capacity and the current source SHA without writing. "
1163 "Publish updates the bundle and generated include, not the ROM."));
1167 if (ImGui::BeginPopupModal(
"Publish message source###PublishMessageSource",
1168 nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
1170 "%s", tr(
"Write the expanded messages into the canonical bundle and "
1171 "generated ASM include. The ROM is not modified."));
1175 if (ImGui::Button(tr(
"Publish now"))) {
1177 ImGui::CloseCurrentPopup();
1180 if (ImGui::Button(tr(
"Cancel"))) {
1181 ImGui::CloseCurrentPopup();
1195 ImGui::BeginDisabled(!can_navigate);
1199 ImGui::EndDisabled();
1200 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1201 ImGui::SetTooltip(
"Previous message (Alt+Up)");
1204 ImGui::BeginDisabled(!can_navigate);
1208 ImGui::EndDisabled();
1209 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1210 ImGui::SetTooltip(
"Next message (Alt+Down)");
1232 : tr(
"Show reference"))) {
1235 if (ImGui::IsItemHovered()) {
1236 ImGui::SetTooltip(
"Show the text from when this message was loaded.");
1241 if (ImGui::Button(tr(
"Accept"))) {
1244 ImGui::EndDisabled();
1245 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1247 "Make the current draft the loaded reference for this message.");
1254 if (ImGui::IsItemHovered()) {
1255 ImGui::SetTooltip(
"Find and replace (F3 finds the next match)");
1258 if (ImGui::Button(tr(
"Optimize"))) {
1261 if (ImGui::IsItemHovered()) {
1263 "Replace dictionary phrases in this message with [D:XX] tokens.");
1271 if (!path.empty()) {
1275 if (ImGui::IsItemHovered()) {
1276 ImGui::SetTooltip(
"Import message bundle");
1281 if (!path.empty()) {
1286 absl::StrFormat(
"Export failed: %s", status.message());
1294 if (ImGui::IsItemHovered()) {
1295 ImGui::SetTooltip(
"Export message bundle");
1299 ImGui::SetNextItemWidth(
1300 std::max(120.0f, ImGui::GetContentRegionAvail().x - 120.0f));
1301 ImGui::InputTextWithHint(
"##message_filter",
"Filter by text, ID, or address",
1304 ImGui::SetNextItemWidth(110.0f);
1306 "All\0Vanilla\0Expanded\0");
1316 ImGui::TextDisabled(
"%d / %d",
static_cast<int>(rows.size()), total);
1320 "Load a ROM to edit dialogue.",
1321 nullptr,
nullptr,
true});
1327 empty.
title =
"No matches";
1328 empty.
detail =
"No messages match this filter.";
1339 constexpr ImGuiTableFlags kListFlags =
1340 ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders |
1341 ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg |
1342 ImGuiTableFlags_ScrollY;
1343 if (!BeginTable(
"##MessagesTable", 4, kListFlags, ImVec2(0, 0))) {
1346 TableSetupColumn(
"ID", ImGuiTableColumnFlags_WidthFixed, 64);
1347 TableSetupColumn(
"Bank", ImGuiTableColumnFlags_WidthFixed, 84);
1348 TableSetupColumn(
"Contents", ImGuiTableColumnFlags_WidthStretch);
1349 TableSetupColumn(
"Address", ImGuiTableColumnFlags_WidthFixed, 90);
1353 int target_row = -1;
1354 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
1355 if (rows[i].display_id == current_id &&
1365 ImGuiListClipper clipper;
1366 clipper.Begin(
static_cast<int>(rows.size()));
1368 clipper.IncludeItemByIndex(target_row);
1370 while (clipper.Step()) {
1371 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
1373 const bool selected = row == target_row;
1374 ImGui::TableNextRow();
1378 ImGui::PushID(message.
expanded ?
"expanded" :
"vanilla");
1381 std::snprintf(id_label,
sizeof(id_label),
"$%04X",
1383 if (ImGui::Selectable(id_label, selected,
1384 ImGuiSelectableFlags_SpanAllColumns |
1385 ImGuiSelectableFlags_AllowOverlap)) {
1390 }
else if (!selected) {
1396 address = message.
address]() {
1397 std::vector<gui::MenuItemSpec> items;
1398 auto open_item = gui::MenuItemSpec::Conditional(
1400 [this, display_id]() {
1401 FinalizePendingUndo();
1402 OpenMessageById(display_id);
1404 [
this]() { return current_parse_errors_.empty(); });
1406 open_item.separator_after =
true;
1407 items.push_back(std::move(open_item));
1410 if (display_id >= 0 &&
1420 ImGui::SetScrollHereY(0.25f);
1425 message.expanded ? tr(
"Expanded") : tr(
"Vanilla"),
1429 const std::string preview =
1431 ImGui::TextUnformatted(preview.c_str());
1434 ImGui::TextUnformatted(
util::HexLong(message.address).c_str());
1442void MessageEditor::DrawMessageStructure() {
1443 if (!ImGui::CollapsingHeader(tr(
"Message Structure"),
1444 ImGuiTreeNodeFlags_DefaultOpen)) {
1447 ImGui::Text(tr(
"Lines: %d"), message_preview_.text_line + 1);
1449 int scroll_count = 0;
1450 int current_line_chars = 0;
1454 for (
size_t i = 0; i < current_message_.Data.size(); i++) {
1455 const uint8_t
byte = current_message_.Data[i];
1459 tr(
" [V] Scroll at byte %zu (line %d, %d chars)"), i,
1460 line_num, current_line_chars);
1461 current_line_chars = 0;
1463 }
else if (
byte ==
kLine1) {
1464 ImGui::TextColored(line_color, tr(
" [1] Line 1 at byte %zu"), i);
1465 current_line_chars = 0;
1467 }
else if (
byte ==
kLine2) {
1468 ImGui::TextColored(line_color, tr(
" [2] Line 2 at byte %zu"), i);
1469 current_line_chars = 0;
1471 }
else if (
byte ==
kLine3) {
1472 ImGui::TextColored(line_color, tr(
" [3] Line 3 at byte %zu"), i);
1473 current_line_chars = 0;
1475 }
else if (
byte < 100) {
1476 current_line_chars++;
1480 if (scroll_count == 0) {
1481 ImGui::TextDisabled(tr(
"No scroll breaks in this message"));
1483 ImGui::Text(tr(
"Total scroll breaks: %d"), scroll_count);
1487 ImGui::TextDisabled(tr(
"Line width budget (max ~170px):"));
1488 const int estimated_line_width = current_line_chars * 8;
1489 const float width_ratio =
static_cast<float>(estimated_line_width) / 170.0f;
1493 ImGui::TextColored(width_color, tr(
"Last line: ~%dpx / 170px (%d chars)"),
1494 estimated_line_width, current_line_chars);
1497void MessageEditor::DrawCurrentMessage() {
1498 DrawMessageHeader();
1501 const float available = ImGui::GetContentRegionAvail().y;
1502 const float max_editor = std::max(96.0f, available - 160.0f);
1503 editor_pane_height_ = std::clamp(editor_pane_height_, 96.0f, max_editor);
1505 if (ImGui::BeginChild(
"##MessageTextPane", ImVec2(0, editor_pane_height_),
1507 DrawDialogueTextPane(ImGui::GetContentRegionAvail().y);
1511 ImGui::InvisibleButton(
"##message_editor_split", ImVec2(-1.0f, 6.0f));
1512 if (ImGui::IsItemHovered() || ImGui::IsItemActive()) {
1513 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
1515 if (ImGui::IsItemActive()) {
1516 editor_pane_height_ += ImGui::GetIO().MouseDelta.y;
1519 if (!current_parse_errors_.empty() || !current_parse_warnings_.empty()) {
1520 if (ImGui::BeginChild(
"##MessageDiagnostics", ImVec2(0, 72),
true)) {
1521 if (!current_parse_errors_.empty()) {
1523 for (
const auto& error : current_parse_errors_) {
1524 ImGui::BulletText(
"%s", error.c_str());
1527 if (!current_parse_warnings_.empty()) {
1529 tr(
"Message parse warnings"));
1530 for (
const auto& warning : current_parse_warnings_) {
1531 ImGui::BulletText(
"%s", warning.c_str());
1538 if (!line_warnings.empty()) {
1540 for (
const auto& warning : line_warnings) {
1541 ImGui::BulletText(
"%s", warning.c_str());
1547 if (ImGui::BeginChild(
"##MessagePreview", ImVec2(0, 0),
true)) {
1548 EnsureFontTexturesReady();
1549 Text(tr(
"Message Preview"));
1551 if (Button(tr(
"View Palette"))) {
1552 ImGui::OpenPopup(
"Palette");
1554 if (ImGui::BeginPopup(
"Palette")) {
1559 ImGui::TextDisabled(tr(
"Scroll the preview with the mouse wheel"));
1562 BeginChild(
"CurrentGfxFont", ImVec2(0, 200),
true,
1563 ImGuiWindowFlags_NoScrollWithMouse);
1564 current_font_gfx16_canvas_.GetConfig().role =
1566 current_font_gfx16_canvas_.DrawBackground();
1568 current_font_gfx16_canvas_.DrawContextMenu();
1570 if (ImGui::IsWindowHovered()) {
1571 const float wheel = ImGui::GetIO().MouseWheel;
1572 if (wheel > 0 && message_preview_.shown_lines > 0) {
1573 message_preview_.shown_lines--;
1574 }
else if (wheel < 0 && message_preview_.shown_lines <
1575 message_preview_.text_line - 2) {
1576 message_preview_.shown_lines++;
1580 const ImVec2 preview_canvas_size = current_font_gfx16_canvas_.canvas_size();
1581 const float dest_width = std::max(0.0f, preview_canvas_size.x - 8.0f);
1582 const float dest_height = std::max(0.0f, preview_canvas_size.y - 8.0f);
1583 float src_height = 0.0f;
1584 if (dest_width > 0.0f && dest_height > 0.0f) {
1585 const float src_width =
1587 src_height = std::min(dest_height * 0.5f,
1589 current_font_gfx16_canvas_.DrawBitmap(
1590 current_font_gfx16_bitmap_, ImVec2(0, 0),
1591 ImVec2(dest_width, dest_height),
1592 ImVec2(0, message_preview_.shown_lines * 16),
1593 ImVec2(src_width, src_height));
1597 ImDrawList* overlay_draw_list = ImGui::GetWindowDrawList();
1598 const ImVec2 canvas_p0 = current_font_gfx16_canvas_.zero_point();
1599 const ImVec2 canvas_sz = current_font_gfx16_canvas_.canvas_size();
1600 const float line_height = 16.0f;
1601 float scale_y = 1.0f;
1602 if (dest_height > 0.0f && src_height > 0.0f) {
1603 scale_y = dest_height / src_height;
1605 const ImU32 marker_color =
1607 for (
int marker_line : message_preview_.scroll_marker_lines) {
1609 (marker_line - message_preview_.shown_lines) * line_height;
1610 const float y = canvas_p0.y + src_y * scale_y;
1611 if (y >= canvas_p0.y && y <= canvas_p0.y + canvas_sz.y) {
1612 overlay_draw_list->AddLine(ImVec2(canvas_p0.x, y),
1613 ImVec2(canvas_p0.x + canvas_sz.x, y),
1614 marker_color, 1.5f);
1615 overlay_draw_list->AddText(
1616 ImVec2(canvas_p0.x + canvas_sz.x + 4, y - 6), marker_color,
1622 current_font_gfx16_canvas_.DrawGrid();
1623 current_font_gfx16_canvas_.DrawOverlay();
1625 DrawMessageStructure();
1630void MessageEditor::DrawFontAtlas() {
1633 EnsureFontTexturesReady();
1636 font_gfx_canvas_.DrawBitmap(font_gfx_bitmap_, 0, 0, 2.0f);
1637 font_gfx_canvas_.DrawTileSelector(16, 32);
1639 ImGui::TextDisabled(
1640 tr(
"2bpp dialogue font at $070000. Click a tile to inspect it."));
1643void MessageEditor::DrawExpandedMessageSettings() {
1648 if (HasMessageSourceLayout()) {
1651 tr(
"Expanded dialogue loads automatically from the project bundle. "
1652 "Reload it after resolving a source error. Inspecting the built ROM "
1654 "manifest data range and does not replace the canonical source."));
1655 if (ImGui::Button(tr(
"Load canonical source"))) {
1656 const auto status = LoadCanonicalExpandedSource();
1658 message_bundle_status_ = std::string(status.message());
1659 message_bundle_status_error_ =
true;
1664 if (ImGui::Button(HasMessageSourceLayout() ? tr(
"Inspect built ROM")
1665 : tr(
"Load from ROM"))) {
1666 auto status = LoadExpandedMessagesFromRom();
1668 LOG_WARN(
"MessageEditor",
"Load from ROM: %s",
1669 std::string(status.message()).c_str());
1670 message_bundle_status_ =
1671 absl::StrFormat(
"Load from ROM failed: %s", status.message());
1672 message_bundle_status_error_ =
true;
1676 if (ImGui::Button(tr(
"Load from File"))) {
1678 if (!path.empty()) {
1679 const auto status = LoadExpandedMessagesFromBin(path);
1681 message_bundle_status_ = std::string(status.message());
1682 message_bundle_status_error_ =
true;
1683 if (
auto* popup_manager = dependencies_.popup_manager) {
1684 popup_manager->Show(
"Error");
1690 if (expanded_messages_.empty()) {
1691 ImGui::TextDisabled(
1692 tr(
"No expanded bank loaded. Vanilla dialogue is still editable."));
1696 ImGui::Text(tr(
"Source: %s"), expanded_message_path_.c_str());
1697 ImGui::Text(tr(
"Messages: %lu"), expanded_messages_.size());
1699 const auto inspection_range = ExpandedInspectionRange();
1700 const int capacity = inspection_range.second - inspection_range.first + 1;
1701 const int used = CalculateExpandedBankUsage();
1702 const int remaining = capacity - used;
1703 const float usage_ratio =
1704 capacity > 0 ?
static_cast<float>(used) /
static_cast<float>(capacity)
1711 char capacity_label[64];
1712 std::snprintf(capacity_label,
sizeof(capacity_label),
"%d / %d bytes", used,
1714 ImGui::ProgressBar(std::clamp(usage_ratio, 0.0f, 1.0f), ImVec2(-1, 0),
1717 ImGui::TextDisabled(tr(
"%d bytes free"), remaining);
1719 if (ImGui::Button(tr(
"Add New Message"))) {
1721 new_message.
ID = expanded_messages_.back().ID + 1;
1723 expanded_messages_.back().Address +
1724 static_cast<int>(expanded_messages_.back().Data.size());
1725 expanded_messages_.push_back(new_message);
1726 MarkDomainDirty(SaveDomain::kExpandedMessages);
1727 const int display_id = expanded_message_base_id_ + new_message.
ID;
1728 if (display_id >= 0 &&
1729 static_cast<size_t>(display_id) >= parsed_messages_.size()) {
1730 parsed_messages_.resize(display_id + 1);
1732 if (current_parse_errors_.empty()) {
1733 FinalizePendingUndo();
1734 OpenMessageById(display_id);
1738 if (ImGui::Button(tr(
"Export to JSON"))) {
1740 if (!path.empty()) {
1745 DrawMessageSourceSync();
1748void MessageEditor::DrawTokenInserter(
const std::vector<TextElement>& elements,
1749 std::string& filter,
bool use_parameter) {
1750 ImGui::SetNextItemWidth(-1);
1751 ImGui::InputTextWithHint(
"##token_filter",
"Filter tokens", &filter);
1755 TableSetupColumn(
"Token", ImGuiTableColumnFlags_WidthFixed, 110);
1756 TableSetupColumn(
"Description", ImGuiTableColumnFlags_WidthStretch);
1758 for (
const auto& text_element : elements) {
1759 if (!filter.empty() &&
1760 !ContainsCaseInsensitive(text_element.GenericToken, filter) &&
1761 !ContainsCaseInsensitive(text_element.Description, filter)) {
1764 ImGui::TableNextRow();
1766 ImGui::PushID(text_element.ID);
1767 const std::string token =
1768 use_parameter ? text_element.GetParamToken(command_parameter_)
1769 : text_element.GenericToken;
1770 if (ImGui::Selectable(text_element.GenericToken.c_str(),
false,
1771 ImGuiSelectableFlags_SpanAllColumns)) {
1772 InsertTokenAtCursor(token);
1774 if (ImGui::IsItemHovered()) {
1775 ImGui::SetTooltip(
"Insert %s", token.c_str());
1779 TextWrapped(
"%s", text_element.Description.c_str());
1784void MessageEditor::DrawTextCommands() {
1786 if (ImGui::IsItemHovered()) {
1787 ImGui::SetTooltip(
"Argument byte for commands such as [W:02] or [SPD:00].");
1789 DrawTokenInserter(TextCommands, command_filter_,
true);
1792void MessageEditor::DrawSpecialCharacters() {
1793 DrawTokenInserter(SpecialChars, symbol_filter_,
false);
1796void MessageEditor::DrawDictionary() {
1797 ImGui::SetNextItemWidth(-1);
1798 ImGui::InputTextWithHint(
"##dictionary_filter",
"Filter dictionary",
1799 &dictionary_filter_);
1800 std::vector<int> visible;
1801 visible.reserve(message_preview_.all_dictionaries_.size());
1803 i < static_cast<int>(message_preview_.all_dictionaries_.size()); ++i) {
1804 const auto& dictionary = message_preview_.all_dictionaries_[i];
1805 if (!dictionary_filter_.empty() &&
1806 !ContainsCaseInsensitive(dictionary.Contents, dictionary_filter_) &&
1807 !ContainsCaseInsensitive(dictionary.Token, dictionary_filter_)) {
1810 visible.push_back(i);
1812 if (visible.empty()) {
1813 ImGui::TextDisabled(tr(
"No dictionary entries match."));
1816 if (!BeginTable(
"##Dictionary", 3,
1820 TableSetupColumn(
"Token", ImGuiTableColumnFlags_WidthFixed, 72);
1821 TableSetupColumn(
"Text", ImGuiTableColumnFlags_WidthStretch);
1822 TableSetupColumn(
"Len", ImGuiTableColumnFlags_WidthFixed, 40);
1825 ImGuiListClipper clipper;
1826 clipper.Begin(
static_cast<int>(visible.size()));
1827 while (clipper.Step()) {
1828 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
1829 const auto& dictionary = message_preview_.all_dictionaries_[visible[row]];
1830 ImGui::TableNextRow();
1832 ImGui::PushID(dictionary.ID);
1833 if (ImGui::Selectable(dictionary.Token.c_str(),
false,
1834 ImGuiSelectableFlags_SpanAllColumns)) {
1835 InsertTokenAtCursor(dictionary.Token);
1837 if (ImGui::IsItemHovered()) {
1838 ImGui::SetTooltip(
"Insert %s", dictionary.Token.c_str());
1842 Text(
"%s", dictionary.Contents.c_str());
1844 Text(
"%d", dictionary.Length);
1850void MessageEditor::DrawMessageTools() {
1851 if (!ImGui::BeginTabBar(
"##MessageTools")) {
1854 if (ImGui::BeginTabItem(tr(
"Commands"))) {
1856 ImGui::EndTabItem();
1858 if (ImGui::BeginTabItem(tr(
"Symbols"))) {
1859 DrawSpecialCharacters();
1860 ImGui::EndTabItem();
1862 if (ImGui::BeginTabItem(tr(
"Dictionary"))) {
1864 ImGui::EndTabItem();
1868void MessageEditor::UpdateCurrentMessageFromText(
const std::string& text) {
1871 MarkDomainDirty(current_message_is_expanded_ ? SaveDomain::kExpandedMessages
1872 : SaveDomain::kVanillaMessages);
1875 current_parse_errors_ = parse_result.errors;
1876 current_parse_warnings_ = parse_result.warnings;
1877 if (!parse_result.ok()) {
1881 std::string raw_text = text;
1882 raw_text.erase(std::remove(raw_text.begin(), raw_text.end(),
'\n'),
1885 current_message_.RawString = raw_text;
1886 current_message_.ContentsParsed = text;
1887 current_message_.Data = std::move(parse_result.bytes);
1889 int parsed_index = current_message_index_;
1890 if (current_message_is_expanded_) {
1891 parsed_index = expanded_message_base_id_ + current_message_index_;
1894 if (parsed_index >= 0) {
1895 if (
static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
1896 parsed_messages_.resize(parsed_index + 1);
1898 parsed_messages_[parsed_index] = text;
1901 if (current_message_is_expanded_) {
1902 if (current_message_index_ >= 0 &&
1903 current_message_index_ <
static_cast<int>(expanded_messages_.size())) {
1904 expanded_messages_[current_message_index_] = current_message_;
1907 if (current_message_index_ >= 0 &&
1908 current_message_index_ <
static_cast<int>(list_of_texts_.size())) {
1909 list_of_texts_[current_message_index_] = current_message_;
1913 DrawMessagePreview();
1916void MessageEditor::ImportMessageBundleFromFile(
const std::string& path) {
1917 message_bundle_status_.clear();
1918 message_bundle_status_error_ =
false;
1921 if (!entries_or.ok()) {
1922 message_bundle_status_ =
1923 absl::StrFormat(
"Import failed: %s", entries_or.status().message());
1924 message_bundle_status_error_ =
true;
1929 const bool rom_dirty_before = rom_ && rom_->dirty();
1930 const bool importing_canonical = IsCanonicalBundlePath(path);
1931 std::string canonical_sha;
1932 if (importing_canonical) {
1933 const auto sha_or = HashFileSha256(std::filesystem::path(path));
1935 message_bundle_status_ = std::string(sha_or.status().message());
1936 message_bundle_status_error_ =
true;
1939 canonical_sha = sha_or.value();
1945 int duplicate_errors = 0;
1946 int parse_error_entries = 0;
1947 int vanilla_updated = 0;
1948 int expanded_updated = 0;
1949 int expanded_created = 0;
1950 bool expanded_modified =
false;
1951 std::vector<std::string> issue_samples;
1953 auto add_issue_sample = [&issue_samples](
const std::string& issue) {
1954 constexpr size_t kMaxIssueSamples = 4;
1955 if (issue_samples.size() < kMaxIssueSamples) {
1956 issue_samples.push_back(issue);
1964 std::unordered_map<std::string, int> seen_entries;
1966 auto entries = entries_or.value();
1967 for (
const auto& entry : entries) {
1968 const std::string entry_key = make_entry_key(entry);
1969 if (seen_entries.find(entry_key) != seen_entries.end()) {
1972 add_issue_sample(absl::StrFormat(
"Duplicate entry for %s", entry_key));
1975 seen_entries.emplace(entry_key, 1);
1979 warnings +=
static_cast<int>(parse_result.warnings.size());
1980 warnings +=
static_cast<int>(line_warnings.size());
1982 if (!parse_result.ok()) {
1984 parse_error_entries++;
1985 if (!parse_result.errors.empty()) {
1986 add_issue_sample(absl::StrFormat(
"Parse error for %s: %s", entry_key,
1987 parse_result.errors.front()));
1989 add_issue_sample(absl::StrFormat(
"Parse error for %s", entry_key));
1994 if (entry.bank == MessageBank::kVanilla) {
1995 if (entry.id < 0 || entry.id >=
static_cast<int>(list_of_texts_.size())) {
1998 absl::StrFormat(
"Vanilla ID out of range: %d", entry.id));
2001 auto& message = list_of_texts_[entry.id];
2002 message.RawString = entry.text;
2003 message.ContentsParsed = entry.text;
2004 message.Data = parse_result.bytes;
2005 message.DataParsed = parse_result.bytes;
2006 if (entry.id >= 0 &&
2007 entry.id <
static_cast<int>(parsed_messages_.size())) {
2008 parsed_messages_[entry.id] = entry.text;
2016 absl::StrFormat(
"Expanded ID out of range: %d", entry.id));
2019 if (entry.id >=
static_cast<int>(expanded_messages_.size())) {
2020 const int old_size =
static_cast<int>(expanded_messages_.size());
2021 const int target_size = entry.id + 1;
2022 expanded_messages_.resize(target_size);
2023 for (
int i = old_size; i < target_size; ++i) {
2024 expanded_messages_[i].ID = i;
2026 expanded_created += target_size - old_size;
2028 auto& message = expanded_messages_[entry.id];
2029 message.RawString = entry.text;
2030 message.ContentsParsed = entry.text;
2031 message.Data = parse_result.bytes;
2032 message.DataParsed = parse_result.bytes;
2033 const int parsed_index = expanded_message_base_id_ + entry.id;
2034 if (parsed_index >= 0) {
2035 if (
static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
2036 parsed_messages_.resize(parsed_index + 1);
2038 parsed_messages_[parsed_index] = entry.text;
2040 expanded_modified =
true;
2046 if (expanded_modified) {
2047 int pos = ExpandedInspectionRange().first;
2048 for (
auto& message : expanded_messages_) {
2049 message.Address = pos;
2050 pos +=
static_cast<int>(message.Data.size()) + 1;
2055 message_bundle_status_ = absl::StrFormat(
2056 "Import finished with %d errors (%d applied: vanilla %d updated, "
2057 "expanded %d updated/%d created; %d warnings, %d duplicates, %d "
2059 errors, applied, vanilla_updated, expanded_updated, expanded_created,
2060 warnings, duplicate_errors, parse_error_entries);
2061 if (!issue_samples.empty()) {
2062 message_bundle_status_ = absl::StrFormat(
2063 "%s Example: %s", message_bundle_status_, issue_samples.front());
2065 message_bundle_status_error_ =
true;
2067 message_bundle_status_ = absl::StrFormat(
2068 "Imported %d messages (vanilla %d updated, expanded %d updated/%d "
2069 "created, %d warnings).",
2070 applied, vanilla_updated, expanded_updated, expanded_created, warnings);
2073 RestoreExpandedDraft(snapshot);
2075 rom_->set_dirty(rom_dirty_before);
2079 int current_display_id = current_message_index_;
2080 if (current_message_is_expanded_) {
2081 current_display_id = expanded_message_base_id_ + current_message_index_;
2083 if (current_display_id >= 0) {
2084 OpenMessageById(current_display_id);
2086 if (vanilla_updated > 0) {
2087 MarkDomainDirty(SaveDomain::kVanillaMessages);
2089 if (expanded_modified) {
2090 if (importing_canonical) {
2091 source_loaded_canonical_sha_ = canonical_sha;
2092 dirty_state_.expanded_messages =
false;
2093 InvalidateSourcePreview();
2094 expanded_message_path_ = path;
2096 source_loaded_canonical_sha_.clear();
2097 MarkDomainDirty(SaveDomain::kExpandedMessages);
2102void MessageEditor::DrawMessagePreview() {
2104 message_preview_.DrawMessagePreview(current_message_);
2107 if (message_preview_.current_preview_data_.empty()) {
2108 LOG_WARN(
"MessageEditor",
"Preview data is empty, skipping bitmap update");
2112 if (current_font_gfx16_bitmap_.is_active()) {
2115 current_font_gfx16_bitmap_.set_data(message_preview_.current_preview_data_);
2117 if (current_font_gfx16_bitmap_.palette().empty() &&
2118 !font_preview_colors_.empty()) {
2119 current_font_gfx16_bitmap_.SetPalette(font_preview_colors_);
2123 if (!current_font_gfx16_bitmap_.surface()) {
2124 LOG_ERROR(
"MessageEditor",
"Bitmap surface is null after set_data()");
2129 const auto command = current_font_gfx16_bitmap_.texture()
2136 "Updated message preview bitmap (size: %zu) and queued texture update",
2137 message_preview_.current_preview_data_.size());
2142 message_preview_.current_preview_data_);
2143 current_font_gfx16_bitmap_.SetPalette(font_preview_colors_);
2148 "Created message preview bitmap (%dx%d) with 8-bit depth and "
2149 "queued texture creation",
2154absl::Status MessageEditor::Save() {
2155 if (!rom_ || !rom_->is_loaded()) {
2156 return absl::FailedPreconditionError(
"ROM not loaded");
2158 if (!current_parse_errors_.empty()) {
2159 return absl::FailedPreconditionError(absl::StrFormat(
2160 "Current message has parse errors: %s", current_parse_errors_.front()));
2163 return SaveDirtyDomains(
true,
2168absl::StatusOr<MessageEditor::SavePlan> MessageEditor::BuildSavePlan(
2169 bool include_font_widths,
bool include_vanilla_messages,
2170 bool include_expanded_messages)
const {
2171 if (!rom_ || !rom_->is_loaded()) {
2172 return absl::FailedPreconditionError(
"ROM not loaded");
2177 if (include_font_widths && dirty_state_.font_widths) {
2181 std::vector<uint8_t>(message_preview_.width_array.begin(),
2182 message_preview_.width_array.end())});
2185 if (include_vanilla_messages && dirty_state_.vanilla_messages) {
2186 std::optional<size_t> expected_message_count;
2187 if (dependencies_.project &&
2188 dependencies_.project->hack_manifest.loaded()) {
2189 const int manifest_count =
2190 dependencies_.project->hack_manifest.message_layout().vanilla_count;
2191 if (manifest_count > 0) {
2192 expected_message_count =
static_cast<size_t>(manifest_count);
2200 for (
const auto& write : vanilla_plan.
writes()) {
2202 {SaveDomain::kVanillaMessages, write.start(), write.bytes()});
2206 if (include_expanded_messages && dirty_state_.expanded_messages &&
2207 !expanded_messages_.empty()) {
2208 const auto range = ExpandedInspectionRange();
2209 const int start = range.first;
2210 const int end = range.second;
2211 if (start < 0 || end < start) {
2212 return absl::InvalidArgumentError(
"Invalid expanded message region");
2214 if (
static_cast<uint64_t
>(start) >= rom_->size() ||
2215 static_cast<uint64_t
>(end) >= rom_->size()) {
2216 return absl::OutOfRangeError(
2217 "Expanded message region is outside the ROM");
2220 const int64_t capacity =
static_cast<int64_t
>(end) - start + 1;
2221 std::vector<uint8_t> bytes;
2222 bytes.reserve(
static_cast<size_t>(capacity));
2225 for (
size_t index = 0; index < expanded_messages_.size(); ++index) {
2226 const auto& message = expanded_messages_[index];
2229 return absl::InvalidArgumentError(
2230 absl::StrFormat(
"Expanded message %d is invalid: %s",
2231 static_cast<int>(index), parsed.errors.front()));
2233 if (message.RawString.find(
"[BANK]") != std::string::npos) {
2234 return absl::InvalidArgumentError(absl::StrFormat(
2235 "Expanded message %d contains [BANK], which is only valid in the "
2236 "vanilla message stream",
2237 static_cast<int>(index)));
2240 const int64_t needed =
static_cast<int64_t
>(parsed.bytes.size()) + 1;
2241 if (
static_cast<int64_t
>(bytes.size()) + needed + 1 > capacity) {
2242 return absl::ResourceExhaustedError(absl::StrFormat(
2243 "Expanded message data exceeds bank boundary "
2244 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
2245 static_cast<int>(index),
static_cast<int>(bytes.size()),
2246 static_cast<int>(needed),
static_cast<int>(capacity), end));
2250 static_cast<int>(bytes.size()));
2251 bytes.insert(bytes.end(), parsed.bytes.begin(), parsed.bytes.end());
2254 bytes.push_back(0xFF);
2257 plan.
writes.push_back({SaveDomain::kExpandedMessages,
2258 static_cast<uint32_t
>(start), std::move(bytes)});
2261 for (
const auto& write : plan.
writes) {
2262 const uint64_t end =
2263 static_cast<uint64_t
>(write.start) + write.bytes.size();
2264 if (end > rom_->size() || end > std::numeric_limits<uint32_t>::max()) {
2265 return absl::OutOfRangeError(absl::StrFormat(
2266 "Message save range [0x%06X, 0x%06llX) is outside the ROM",
2267 write.start,
static_cast<unsigned long long>(end)));
2274absl::Status MessageEditor::ValidateSavePlan(
const SavePlan& plan)
const {
2275 if (!dependencies_.project ||
2276 !dependencies_.project->hack_manifest.loaded() || plan.
writes.empty()) {
2277 return absl::OkStatus();
2280 std::vector<std::pair<uint32_t, uint32_t>> ranges;
2281 ranges.reserve(plan.
writes.size());
2282 for (
const auto& write : plan.
writes) {
2283 ranges.emplace_back(write.start, write.end());
2286 const auto& yaze_project = *dependencies_.project;
2287 if (yaze_project.rom_metadata.write_policy ==
2290 absl::StrContains(yaze_project.hack_manifest.hack_name(),
2291 "Oracle of Secrets")) {
2296 yaze_project.hack_manifest, yaze_project.rom_metadata.write_policy,
2297 ranges,
"message data",
"MessageEditor", dependencies_.toast_manager);
2301absl::Status MessageEditor::ApplySavePlan(
const SavePlan& plan) {
2302 if (plan.
writes.empty()) {
2303 return absl::OkStatus();
2307 for (
const auto& write : plan.
writes) {
2308 const char* label =
"MessageData";
2309 switch (write.domain) {
2310 case SaveDomain::kFontWidths:
2311 label =
"MessageFontWidths";
2313 case SaveDomain::kVanillaMessages:
2314 label =
"VanillaMessageBank";
2316 case SaveDomain::kExpandedMessages:
2317 label =
"ExpandedMessageBank";
2325 for (
const auto& write : plan.
writes) {
2327 rom_->WriteVector(
static_cast<int>(write.start), write.bytes));
2332 for (
size_t index = 0; index < expanded_messages_.size() &&
2335 expanded_messages_[index].Address =
2338 SyncCurrentExpandedMessageAddress();
2341 return absl::OkStatus();
2344absl::Status MessageEditor::SaveDirtyDomains(
bool include_font_widths,
2345 bool include_vanilla_messages,
2346 bool include_expanded_messages) {
2348 BuildSavePlan(include_font_widths, include_vanilla_messages,
2349 include_expanded_messages));
2352 RecordSavedDomains(plan);
2353 return absl::OkStatus();
2356absl::Status MessageEditor::SaveExpandedMessages() {
2357 return SaveDirtyDomains(
false,
2362absl::Status MessageEditor::BeginSaveTransaction() {
2363 if (save_transaction_active_) {
2364 return absl::FailedPreconditionError(
2365 "Message save transaction is already active");
2367 transaction_dirty_snapshot_ = dirty_state_;
2368 transaction_saved_domains_ = {};
2369 transaction_expanded_address_snapshot_.clear();
2370 transaction_expanded_address_snapshot_.reserve(expanded_messages_.size());
2371 for (
const auto& message : expanded_messages_) {
2372 transaction_expanded_address_snapshot_.push_back(message.Address);
2374 save_transaction_active_ =
true;
2375 return absl::OkStatus();
2378void MessageEditor::RollbackSaveTransaction() {
2379 if (!save_transaction_active_) {
2382 dirty_state_ = transaction_dirty_snapshot_;
2383 for (
size_t index = 0; index < expanded_messages_.size() &&
2384 index < transaction_expanded_address_snapshot_.size();
2386 expanded_messages_[index].Address =
2387 transaction_expanded_address_snapshot_[index];
2389 SyncCurrentExpandedMessageAddress();
2390 transaction_dirty_snapshot_ = {};
2391 transaction_saved_domains_ = {};
2392 transaction_expanded_address_snapshot_.clear();
2393 save_transaction_active_ =
false;
2396void MessageEditor::CommitSaveTransaction() {
2397 if (!save_transaction_active_) {
2400 ClearSavedDomains(transaction_saved_domains_);
2401 transaction_dirty_snapshot_ = {};
2402 transaction_saved_domains_ = {};
2403 transaction_expanded_address_snapshot_.clear();
2404 save_transaction_active_ =
false;
2408 bool* dirty =
nullptr;
2409 bool* transaction_saved =
nullptr;
2411 case SaveDomain::kFontWidths:
2412 dirty = &dirty_state_.font_widths;
2413 transaction_saved = &transaction_saved_domains_.font_widths;
2415 case SaveDomain::kVanillaMessages:
2416 dirty = &dirty_state_.vanilla_messages;
2417 transaction_saved = &transaction_saved_domains_.vanilla_messages;
2419 case SaveDomain::kExpandedMessages:
2420 dirty = &dirty_state_.expanded_messages;
2421 transaction_saved = &transaction_saved_domains_.expanded_messages;
2425 if (save_transaction_active_) {
2426 *transaction_saved =
false;
2428 if (domain == SaveDomain::kExpandedMessages) {
2429 InvalidateSourcePreview();
2431 if (rom_ && !(domain == SaveDomain::kExpandedMessages &&
2432 ExpandedEditsAreSourceOwned())) {
2433 rom_->set_dirty(
true);
2437void MessageEditor::RecordSavedDomains(
const SavePlan& plan) {
2440 if (!save_transaction_active_) {
2441 ClearSavedDomains(saved);
2444 transaction_saved_domains_.font_widths |= saved.font_widths;
2445 transaction_saved_domains_.vanilla_messages |= saved.vanilla_messages;
2446 transaction_saved_domains_.expanded_messages |= saved.expanded_messages;
2451 dirty_state_.font_widths =
false;
2454 dirty_state_.vanilla_messages =
false;
2457 dirty_state_.expanded_messages =
false;
2461void MessageEditor::SyncCurrentExpandedMessageAddress() {
2462 if (!current_message_is_expanded_ || current_message_index_ < 0 ||
2463 current_message_index_ >=
static_cast<int>(expanded_messages_.size())) {
2466 current_message_.Address = expanded_messages_[current_message_index_].Address;
2469absl::Status MessageEditor::LoadCanonicalExpandedSource() {
2470 if (!HasMessageSourceLayout()) {
2471 return absl::FailedPreconditionError(
2472 "The project hack manifest does not define messages.source");
2474 if (dirty_state_.expanded_messages || dirty_state_.vanilla_messages ||
2475 dirty_state_.font_widths || pending_undo_before_.has_value() ||
2476 !current_parse_errors_.empty()) {
2477 return absl::FailedPreconditionError(
2478 "The Message editor has pending edits; canonical source was not "
2482 if (!source_or.ok()) {
2483 return source_or.status();
2487 expanded_message_base_id_ = source_or->first_expanded_id;
2488 expanded_messages_ = std::move(source_or->messages);
2489 expanded_message_path_ = source_or->canonical_bundle_path.string();
2490 source_loaded_canonical_sha_ = std::move(source_or->source_sha256);
2491 parsed_messages_.resize(expanded_message_base_id_ +
2492 expanded_messages_.size());
2493 for (
const auto& message : expanded_messages_) {
2494 parsed_messages_[expanded_message_base_id_ + message.ID] =
2497 InvalidateSourcePreview();
2498 NoteLoadedMessages();
2499 message_bundle_status_ =
2500 absl::StrFormat(
"Loaded %zu expanded messages from project source: %s",
2501 expanded_messages_.size(), expanded_message_path_);
2502 message_bundle_status_error_ =
false;
2503 if (current_message_is_expanded_) {
2504 OpenMessageById(expanded_message_base_id_ + current_message_index_);
2506 return absl::OkStatus();
2509absl::Status MessageEditor::LoadExpandedMessagesFromBin(
2510 const std::string& path) {
2512 const bool rom_dirty_before = rom_ && rom_->dirty();
2513 expanded_message_base_id_ = ResolveExpandedMessageBaseId();
2514 parsed_messages_.resize(
2515 static_cast<size_t>(std::max(expanded_message_base_id_, 0)));
2516 expanded_messages_.clear();
2517 std::vector<std::string> parsed_expanded;
2518 std::string mutable_path = path;
2521 message_preview_.all_dictionaries_);
2522 if (!status.ok() || expanded_messages_.empty()) {
2523 RestoreExpandedDraft(snapshot);
2525 rom_->set_dirty(rom_dirty_before);
2530 return absl::NotFoundError(
"No expanded messages found in file");
2532 parsed_messages_.insert(parsed_messages_.end(), parsed_expanded.begin(),
2533 parsed_expanded.end());
2534 expanded_message_path_ = path;
2535 source_loaded_canonical_sha_.clear();
2536 NoteLoadedMessages();
2537 MarkDomainDirty(SaveDomain::kExpandedMessages);
2538 return absl::OkStatus();
2541absl::Status MessageEditor::LoadExpandedMessagesFromRom() {
2542 if (!rom_ || !rom_->is_loaded()) {
2543 return absl::FailedPreconditionError(
"ROM not loaded");
2547 const bool rom_dirty_before = rom_ && rom_->dirty();
2548 const auto range = ExpandedInspectionRange();
2549 const int end = std::min(range.second,
static_cast<int>(rom_->size()) - 1);
2551 if (loaded.empty()) {
2552 RestoreExpandedDraft(snapshot);
2554 rom_->set_dirty(rom_dirty_before);
2556 return absl::NotFoundError(
2557 "No expanded messages found in ROM at the manifest data range");
2560 expanded_message_base_id_ = ResolveExpandedMessageBaseId();
2561 parsed_messages_.resize(
2562 static_cast<size_t>(std::max(expanded_message_base_id_, 0)));
2563 expanded_messages_ = std::move(loaded);
2564 auto parsed_expanded =
2566 for (
const auto& msg : expanded_messages_) {
2567 if (msg.ID >= 0 && msg.ID <
static_cast<int>(parsed_expanded.size())) {
2568 parsed_messages_.push_back(parsed_expanded[msg.ID]);
2572 expanded_message_path_ =
"(ROM inspection)";
2573 source_loaded_canonical_sha_.clear();
2574 dirty_state_.expanded_messages =
false;
2575 InvalidateSourcePreview();
2576 NoteLoadedMessages();
2577 return absl::OkStatus();
2580int MessageEditor::CalculateExpandedBankUsage()
const {
2581 if (expanded_messages_.empty())
2584 for (
const auto& msg : expanded_messages_) {
2585 total +=
static_cast<int>(msg.Data.size()) + 1;
2591absl::Status MessageEditor::Cut() {
2593 if (!message_text_box_.text.empty()) {
2595 message_text_box_.Cut();
2597 return absl::OkStatus();
2600absl::Status MessageEditor::Paste() {
2602 if (ImGui::GetClipboardText() !=
nullptr) {
2604 message_text_box_.Paste();
2606 return absl::OkStatus();
2609absl::Status MessageEditor::Copy() {
2611 if (message_text_box_.selection_length > 0) {
2613 message_text_box_.Copy();
2615 return absl::OkStatus();
2618void MessageEditor::PushUndoSnapshot() {
2619 if (pending_undo_before_.has_value()) {
2622 if (pending_undo_before_->message_index == current_message_index_ &&
2623 pending_undo_before_->is_expanded == current_message_is_expanded_) {
2626 FinalizePendingUndo();
2630 int parsed_index = current_message_index_;
2631 if (current_message_is_expanded_) {
2632 parsed_index = expanded_message_base_id_ + current_message_index_;
2635 if (parsed_index >= 0 &&
2636 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2637 text = parsed_messages_[parsed_index];
2639 pending_undo_before_ =
2641 current_message_is_expanded_};
2644void MessageEditor::FinalizePendingUndo() {
2645 if (!pending_undo_before_.has_value())
2650 const int message_index = pending_undo_before_->
message_index;
2651 const bool is_expanded = pending_undo_before_->is_expanded;
2655 if (message_index < 0 ||
2656 message_index >=
static_cast<int>(expanded_messages_.size())) {
2657 pending_undo_before_.reset();
2660 after_message = expanded_messages_[message_index];
2662 if (message_index < 0 ||
2663 message_index >=
static_cast<int>(list_of_texts_.size())) {
2664 pending_undo_before_.reset();
2667 after_message = list_of_texts_[message_index];
2670 int parsed_index = message_index;
2672 parsed_index = expanded_message_base_id_ + message_index;
2675 if (parsed_index >= 0 &&
2676 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2677 text = parsed_messages_[parsed_index];
2680 message_index, is_expanded};
2682 undo_manager_.Push(std::make_unique<MessageEditAction>(
2683 std::move(*pending_undo_before_), std::move(after),
2685 pending_undo_before_.reset();
2689 current_message_ = snapshot.
message;
2691 current_message_is_expanded_ = snapshot.
is_expanded;
2693 const auto diagnostics =
2695 current_parse_errors_ = diagnostics.errors;
2696 current_parse_warnings_ = diagnostics.warnings;
2700 parsed_index = expanded_message_base_id_ + snapshot.
message_index;
2702 if (parsed_index >= 0 &&
2703 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2704 parsed_messages_[parsed_index] = snapshot.
parsed_text;
2709 snapshot.
message_index <
static_cast<int>(expanded_messages_.size())) {
2714 snapshot.
message_index <
static_cast<int>(list_of_texts_.size())) {
2719 MarkDomainDirty(snapshot.
is_expanded ? SaveDomain::kExpandedMessages
2720 : SaveDomain::kVanillaMessages);
2721 DrawMessagePreview();
2724absl::Status MessageEditor::Undo() {
2725 FinalizePendingUndo();
2726 return undo_manager_.Undo();
2729absl::Status MessageEditor::Redo() {
2730 return undo_manager_.Redo();
2733void MessageEditor::Delete() {
2735 if (message_text_box_.selection_length == 0) {
2737 message_text_box_.clear();
2741void MessageEditor::SelectAll() {
2743 if (message_text_box_.selection_length == 0) {
2745 message_text_box_.SelectAll();
2748 message_text_box_.Focus();
2752absl::Status MessageEditor::Find() {
2755 find_panel_open_ =
true;
2756 find_focus_search_ =
true;
2757 if (dependencies_.window_manager) {
2758 dependencies_.window_manager->OpenWindow(dependencies_.session_id,
2759 "message.message_editor");
2761 return absl::OkStatus();
2764int MessageEditor::ReplaceCurrentMatch() {
2765 if (search_text_.empty()) {
2769 std::string& text = message_text_box_.text;
2770 const size_t cursor =
static_cast<size_t>(
2771 std::clamp(editor_cursor_, 0,
static_cast<int>(text.size())));
2772 const size_t from = find_has_match_ ? find_match_start_ : cursor;
2775 if (!match && from > 0) {
2783 text.replace(*match, search_text_.size(), replace_text_);
2784 pending_selection_start_ =
static_cast<int>(*match);
2785 pending_selection_end_ =
2786 pending_selection_start_ +
static_cast<int>(replace_text_.size());
2787 editor_cursor_ = pending_selection_end_;
2788 find_has_match_ =
false;
2789 ImGui::ClearActiveID();
2790 UpdateCurrentMessageFromText(text);
2791 FinalizePendingUndo();
2795int MessageEditor::ReplaceAllMatches() {
2796 if (search_text_.empty()) {
2799 if (!current_parse_errors_.empty()) {
2801 "Replace All blocked: resolve the current message parse errors first";
2802 replace_status_error_ =
true;
2806 auto replace_in_text = [&](std::string& text) ->
int {
2808 std::string search = search_text_;
2810 if (!case_sensitive_) {
2811 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
2815 while (pos < text.size()) {
2816 std::string source = text;
2817 if (!case_sensitive_) {
2818 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
2821 size_t found = source.find(search, pos);
2822 if (found == std::string::npos)
2825 if (match_whole_word_) {
2826 bool start_ok = (found == 0 || !std::isalnum(source[found - 1]));
2827 bool end_ok = (found + search.size() >= source.size() ||
2828 !std::isalnum(source[found + search.size()]));
2829 if (!start_ok || !end_ok) {
2835 text.replace(found, search_text_.size(), replace_text_);
2836 pos = found + replace_text_.size();
2842 struct PlannedReplacement {
2848 std::vector<PlannedReplacement> plan;
2850 const auto plan_replacements = [&](
const std::vector<MessageData>& messages,
2851 bool is_expanded) ->
bool {
2852 for (
size_t i = 0; i < messages.size(); ++i) {
2853 const int parsed_index =
2854 (is_expanded ? expanded_message_base_id_ : 0) +
static_cast<int>(i);
2855 if (parsed_index < 0 ||
2856 parsed_index >=
static_cast<int>(parsed_messages_.size())) {
2860 std::string text = parsed_messages_[parsed_index];
2861 const int count = replace_in_text(text);
2868 (is_expanded && text.find(
"[BANK]") != std::string::npos)) {
2869 const std::string error =
2870 !parsed.ok() ? parsed.errors.front()
2871 :
"[BANK] is not valid in expanded messages";
2872 replace_status_ = absl::StrFormat(
2873 "Replace All aborted at %s message %d: %s",
2874 is_expanded ?
"expanded" :
"vanilla",
static_cast<int>(i), error);
2875 replace_status_error_ =
true;
2880 {
static_cast<int>(i), is_expanded, std::move(text), count});
2886 if (!plan_replacements(list_of_texts_,
false) ||
2887 !plan_replacements(expanded_messages_,
true)) {
2891 const int previous_index = current_message_index_;
2892 const bool previous_expanded = current_message_is_expanded_;
2893 int total_replacements = 0;
2895 for (
const auto& replacement : plan) {
2896 current_message_ = replacement.is_expanded
2897 ? expanded_messages_[replacement.message_index]
2898 : list_of_texts_[replacement.message_index];
2899 current_message_index_ = replacement.message_index;
2900 current_message_is_expanded_ = replacement.is_expanded;
2901 message_text_box_.text = replacement.text;
2902 UpdateCurrentMessageFromText(replacement.text);
2903 FinalizePendingUndo();
2904 total_replacements += replacement.count;
2907 current_message_index_ = previous_index;
2908 current_message_is_expanded_ = previous_expanded;
2911 int current_parsed_idx = current_message_index_;
2912 if (current_message_is_expanded_) {
2913 current_parsed_idx = expanded_message_base_id_ + current_message_index_;
2915 if (current_parsed_idx >= 0 &&
2916 current_parsed_idx <
static_cast<int>(parsed_messages_.size())) {
2917 message_text_box_.text = parsed_messages_[current_parsed_idx];
2921 if (current_message_is_expanded_) {
2922 if (current_message_index_ >= 0 &&
2923 current_message_index_ <
static_cast<int>(expanded_messages_.size())) {
2924 current_message_ = expanded_messages_[current_message_index_];
2927 if (current_message_index_ >= 0 &&
2928 current_message_index_ <
static_cast<int>(list_of_texts_.size())) {
2929 current_message_ = list_of_texts_[current_message_index_];
2933 const auto current_diagnostics =
2935 current_parse_errors_ = current_diagnostics.errors;
2936 current_parse_warnings_ = current_diagnostics.warnings;
2938 return total_replacements;
const MessageLayout & message_layout() const
bool loaded() const
Check if the manifest has been loaded.
virtual void SetGameData(zelda3::GameData *game_data)
project::YazeProject * project() const
zelda3::GameData * game_data() const
EditorDependencies dependencies_
void DrawMessageSourceSync()
DirtyState transaction_dirty_snapshot_
MessageSourcePublishButtons DrawMessageSourcePublishButtons()
std::vector< std::string > baseline_messages_
bool source_sync_changed_
void RestoreExpandedDraft(const ExpandedDraftSnapshot &snapshot)
std::vector< std::string > parsed_messages_
std::string reference_view_text_
bool current_message_is_expanded_
bool replace_status_error_
std::string source_loaded_canonical_sha_
std::vector< MessageData > expanded_messages_
void ContributeStatus(StatusBar *status_bar) override
void ResolveFontPalette()
DirtyState transaction_saved_domains_
bool scroll_list_to_selection_
int pending_selection_start_
bool font_graphics_loaded_
bool ExpandedEditsAreSourceOwned() const
absl::Status Update() override
bool IsCanonicalBundlePath(const std::string &path) const
void FinalizePendingUndo()
absl::Status LoadExpandedMessagesFromRom()
void DrawDialogueTextPane(float height)
void DrawExpandedMessageSettings()
int pending_selection_end_
void UpdateCurrentMessageFromText(const std::string &text)
bool save_transaction_active_
std::vector< int > transaction_expanded_address_snapshot_
bool AdvanceFind(bool forward)
ExpandedDraftSnapshot CaptureExpandedDraft() const
void OptimizeCurrentMessage()
std::string source_sync_draft_sha_
bool HasMessageSourceLayout() const
absl::StatusOr< std::filesystem::path > ResolveCanonicalBundlePath() const
std::vector< std::string > current_parse_errors_
absl::Status Load() override
std::string MessageSourceIncomingPath() const
std::string replace_status_
absl::StatusOr< std::string > HashFileSha256(const std::filesystem::path &path) const
void HandleMessageShortcuts()
std::vector< std::string > current_parse_warnings_
std::string message_bundle_status_
std::pair< int, int > ExpandedInspectionRange() const
void Initialize() override
std::string replace_text_
void NoteLoadedMessages()
absl::Status LoadCanonicalExpandedSource()
void InsertTokenAtCursor(const std::string &token)
int CurrentDisplayId() const
void DrawMessagePreview()
std::string source_sync_summary_
std::vector< MessageRow > CollectMessageRows(bool apply_filter) const
bool ActivateFindMatch(const MessageRow &row, size_t offset, bool wrapped)
int expanded_message_base_id_
void PublishMessageSource()
bool message_bundle_status_error_
std::string_view ContentsForDisplayId(int display_id) const
void InvalidateSourcePreview()
std::vector< MessageData > list_of_texts_
std::string CurrentReferenceText() const
std::string source_sync_status_
bool OpenMessageById(int display_id)
gfx::SnesPalette font_preview_colors_
MessageData current_message_
void AcceptCurrentAsReference()
MessagePreview message_preview_
std::string source_sync_sha_before_
gui::TextBox message_text_box_
void DrawCurrentMessage()
bool project_source_load_attempted_
void SelectAdjacentMessage(int delta)
bool source_sync_status_error_
int ResolveExpandedMessageBaseId() const
void ImportMessageBundleFromFile(const std::string &path)
gfx::Bitmap current_font_gfx16_bitmap_
int ReplaceCurrentMatch()
void PreviewMessageSource()
int current_message_index_
std::string expanded_message_path_
void SetGameData(zelda3::GameData *game_data) override
A session-aware status bar displayed at the bottom of the application.
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
const std::vector< VanillaMessageWrite > & writes() const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
SnesPalette * mutable_palette()
RAII timer for automatic timing management.
static void BeginToolbar(const char *label)
static bool ToolbarButton(const char *label, const ImVec2 &size=ImVec2(0, 0))
RAII guard for ImGui style colors.
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 ICON_MD_NAVIGATE_BEFORE
#define ICON_MD_ARROW_FORWARD
#define ICON_MD_FONT_DOWNLOAD
#define ICON_MD_LIBRARY_ADD
#define ICON_MD_NAVIGATE_NEXT
#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)
bool MessageSurfaceFocused()
int MessageInputCallback(ImGuiInputTextCallbackData *data)
constexpr int kCharactersWidth
int GetExpandedTextDataStart()
constexpr uint8_t kScrollVertical
absl::StatusOr< MessageSourceSnapshot > LoadMessageSource(const project::YazeProject &project)
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 MessageListPreview(std::string_view text, size_t max_chars=96)
std::string MessageBankToString(MessageBank bank)
constexpr int kCurrentMessageWidth
constexpr int kCurrentMessageHeight
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
std::string ComputeMessageSourceSha256(std::string_view content)
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
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
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
absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
constexpr char kOracleExpandedRomWriteRefusal[]
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
absl::StatusOr< MessageSourceSyncResult > SyncMessageSource(const project::YazeProject &project, const fs::path &incoming_bundle_path, const MessageSourceSyncOptions &options)
std::optional< size_t > FindNextTextMatch(std::string_view text, std::string_view needle, size_t start, bool case_sensitive, bool whole_word)
std::optional< size_t > FindPreviousTextMatch(std::string_view text, std::string_view needle, size_t before, bool case_sensitive, bool whole_word)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
bool MessageListQueryMatches(int display_id, uint32_t address, std::string_view contents, bool expanded, MessageBankFilter bank, std::string_view query)
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void MemoryEditorPopup(const std::string &label, std::span< uint8_t > memory)
void SectionHeader(const char *icon, const char *label, const ImVec4 &color)
bool ItemContextMenu(const char *id, const std::function< std::vector< MenuItemSpec >()> &build, ImGuiPopupFlags flags)
Right-click menu for the last submitted ImGui item.
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
MenuItemSpec CopyToClipboardItem(const std::string &label, const std::string &text)
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
void StatusBadge(const char *text, ButtonType type)
std::string HexWord(uint16_t word, HexStringParams params)
std::string HexLong(uint32_t dword, HexStringParams params)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
std::optional< Source > source
project::YazeProject * project
WorkspaceWindowManager * window_manager
std::vector< uint8_t > Data
std::string OptimizeMessageForDictionary(std::string_view message_string, const std::vector< DictionaryEntry > &dictionary)
std::string loaded_canonical_sha
std::vector< MessageData > messages
std::vector< std::string > parsed_messages
std::string preview_summary
bool preview_status_error
std::string preview_draft_sha
std::string preview_status
std::vector< MessageData > vanilla_messages
std::string preview_sha_before
std::vector< int > expanded_message_addresses
bool saves_vanilla_messages
bool saves_expanded_messages
std::vector< PlannedWrite > writes
std::array< uint8_t, kWidthArraySize > width_array
std::vector< DictionaryEntry > all_dictionaries_
std::string expected_source_sha256
std::string source_sha256_before
std::string proposed_source_sha256
Shared empty / disabled panel presentation.
const char * action_label
std::function< void()> on_action
bool compact
Tighter vertical rhythm for drawers / inspectors.
core::HackManifest hack_manifest