yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_editor.cc
Go to the documentation of this file.
1#include "message_editor.h"
2
3#include <string>
4#include <vector>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
10#include "app/gfx/core/bitmap.h"
16#include "app/gui/core/icons.h"
17#include "app/gui/core/input.h"
18#include "app/gui/core/style.h"
19#include "rom/rom.h"
20#include "imgui.h"
21#include "imgui/misc/cpp/imgui_stdlib.h"
22#include "util/file_util.h"
23#include "util/hex.h"
24#include "util/log.h"
25
26namespace yaze {
27namespace editor {
28
29namespace {
30std::string DisplayTextOverflowError(int pos, bool bank) {
31 int space = bank ? kTextDataEnd - kTextData : kTextData2End - kTextData2;
32 std::string bankSTR = bank ? "1st" : "2nd";
33 std::string posSTR =
34 bank ? absl::StrFormat("%X4", pos & 0xFFFF)
35 : absl::StrFormat("%X4", (pos - kTextData2) & 0xFFFF);
36 std::string message = absl::StrFormat(
37 "There is too much text data in the %s block to save.\n"
38 "Available: %X4 | Used: %s",
39 bankSTR, space, posSTR);
40 return message;
41}
42} // namespace
43
44using ImGui::BeginChild;
45using ImGui::BeginTable;
46using ImGui::Button;
47using ImGui::EndChild;
48using ImGui::EndTable;
49using ImGui::InputTextMultiline;
50using ImGui::PopID;
51using ImGui::PushID;
52using ImGui::SameLine;
53using ImGui::Separator;
54using ImGui::TableHeadersRow;
55using ImGui::TableNextColumn;
56using ImGui::TableSetupColumn;
57using ImGui::Text;
58using ImGui::TextWrapped;
59
60constexpr ImGuiTableFlags kMessageTableFlags = ImGuiTableFlags_Hideable |
61 ImGuiTableFlags_Borders |
62 ImGuiTableFlags_Resizable;
63
65 // Register panels with PanelManager (dependency injection)
67 return;
68
69 auto* panel_manager = dependencies_.panel_manager;
70 const size_t session_id = dependencies_.session_id;
71
72 // Register EditorPanel implementations (they provide both metadata and drawing)
73 panel_manager->RegisterEditorPanel(std::make_unique<MessageListPanel>(
74 [this]() { DrawMessageList(); }));
75 panel_manager->RegisterEditorPanel(std::make_unique<MessageEditorPanel>(
76 [this]() { DrawCurrentMessage(); }));
77 panel_manager->RegisterEditorPanel(std::make_unique<FontAtlasPanel>([this]() {
80 }));
81 panel_manager->RegisterEditorPanel(std::make_unique<DictionaryPanel>([this]() {
85 }));
86
87 // Show message list by default
88 panel_manager->ShowPanel(session_id, "message.message_list");
89
90 for (int i = 0; i < kWidthArraySize; i++) {
92 }
93
95 list_of_texts_ = ReadAllTextData(rom()->mutable_data());
96 LOG_INFO("MessageEditor", "Loaded %zu messages from ROM", list_of_texts_.size());
97
98 if (game_data()) {
100 }
101
102 for (int i = 0; i < 0x4000; i++) {
103 raw_font_gfx_data_[i] = rom()->data()[kGfxFont + i];
104 }
106 gfx::SnesTo8bppSheet(raw_font_gfx_data_, /*bpp=*/2, /*num_sheets=*/2);
107
108 // Create bitmap for font graphics
113
114 // Queue texture creation - will be processed in render loop
117
118 LOG_INFO("MessageEditor", "Font bitmap created and texture queued");
120
121 auto load_font = zelda3::LoadFontGraphics(*rom());
122 if (load_font.ok()) {
123 message_preview_.font_gfx16_data_2_ = load_font.value().vector();
124 }
127
128 if (!list_of_texts_.empty()) {
129 // Default to message 1 if available, otherwise 0
130 size_t default_idx = list_of_texts_.size() > 1 ? 1 : 0;
131 current_message_ = list_of_texts_[default_idx];
134 } else {
135 LOG_ERROR("MessageEditor", "No messages found in ROM!");
136 }
137}
138
139absl::Status MessageEditor::Load() {
140 gfx::ScopedTimer timer("MessageEditor::Load");
141 return absl::OkStatus();
142}
143
144absl::Status MessageEditor::Update() {
145 // Panel drawing is handled centrally by PanelManager::DrawAllVisiblePanels()
146 // via the EditorPanel implementations registered in Initialize().
147 // No local drawing needed here.
148 return absl::OkStatus();
149}
150
153 if (BeginChild("##MessagesList", ImVec2(0, 0), true,
154 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
156 if (BeginTable("##MessagesTable", 3, kMessageTableFlags)) {
157 TableSetupColumn("ID");
158 TableSetupColumn("Contents");
159 TableSetupColumn("Data");
160
161 TableHeadersRow();
162 for (const auto& message : list_of_texts_) {
163 TableNextColumn();
164 PushID(message.ID);
165 if (Button(util::HexWord(message.ID).c_str())) {
166 current_message_ = message;
169 }
170 PopID();
171 TableNextColumn();
172 TextWrapped("%s", parsed_messages_[message.ID].c_str());
173 TableNextColumn();
174 TextWrapped("%s",
175 util::HexLong(list_of_texts_[message.ID].Address).c_str());
176 }
177 for (const auto& expanded_message : expanded_messages_) {
178 TableNextColumn();
179 PushID(expanded_message.ID + 0x18D);
180 if (Button(util::HexWord(expanded_message.ID + 0x18D).c_str())) {
181 current_message_ = expanded_message;
183 parsed_messages_[expanded_message.ID + 0x18D];
185 }
186 PopID();
187 TableNextColumn();
188 TextWrapped("%s",
189 parsed_messages_[expanded_message.ID + 0x18C].c_str());
190 TableNextColumn();
191 TextWrapped("%s", util::HexLong(expanded_message.Address).c_str());
192 }
193
194 EndTable();
195 }
196 }
197 EndChild();
198}
199
201 Button(absl::StrCat("Message ", current_message_.ID).c_str());
202 if (InputTextMultiline("##MessageEditor", &message_text_box_.text,
203 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
204 std::string temp = message_text_box_.text;
205 // Strip newline characters.
206 temp.erase(std::remove(temp.begin(), temp.end(), '\n'), temp.end());
209 }
210 Separator();
212
213 ImGui::BeginChild("##MessagePreview", ImVec2(0, 0), true, 1);
214 Text("Message Preview");
215 if (Button("View Palette")) {
216 ImGui::OpenPopup("Palette");
217 }
218 if (ImGui::BeginPopup("Palette")) {
220 ImGui::EndPopup();
221 }
223 BeginChild("CurrentGfxFont", ImVec2(348, 0), true,
224 ImGuiWindowFlags_NoScrollWithMouse);
228
229 // Handle mouse wheel scrolling
230 if (ImGui::IsWindowHovered()) {
231 float wheel = ImGui::GetIO().MouseWheel;
232 if (wheel > 0 && message_preview_.shown_lines > 0) {
234 } else if (wheel < 0 &&
237 }
238 }
239
240 // Draw only the visible portion of the text
242 current_font_gfx16_bitmap_, ImVec2(0, 0), // Destination position
243 ImVec2(340,
244 font_gfx_canvas_.canvas_size().y), // Destination size
245 ImVec2(0, message_preview_.shown_lines * 16), // Source position
246 ImVec2(170,
247 font_gfx_canvas_.canvas_size().y / 2) // Source size
248 );
249
252 EndChild();
253 ImGui::EndChild();
254}
255
262
264 ImGui::BeginChild("##ExpandedMessageSettings", ImVec2(0, 100), true,
265 ImGuiWindowFlags_AlwaysVerticalScrollbar);
266 ImGui::Text("Expanded Messages");
267
268 if (ImGui::Button("Load Expanded Message")) {
270 if (!path.empty()) {
275 .ok()) {
276 if (auto* popup_manager = dependencies_.popup_manager) {
277 popup_manager->Show("Error");
278 }
279 }
280 }
281 }
282
283 if (expanded_messages_.size() > 0) {
284 ImGui::Text("Expanded Path: %s", expanded_message_path_.c_str());
285 ImGui::Text("Expanded Messages: %lu", expanded_messages_.size());
286 if (ImGui::Button("Add New Message")) {
287 MessageData new_message;
288 new_message.ID = expanded_messages_.back().ID + 1;
289 new_message.Address = expanded_messages_.back().Address +
290 expanded_messages_.back().Data.size();
291 expanded_messages_.push_back(new_message);
292 }
293
294 if (ImGui::Button("Save Expanded Messages")) {
295 if (expanded_message_path_.empty()) {
297 }
298 if (!expanded_message_path_.empty()) {
300 }
301 }
302
303 ImGui::SameLine();
304 if (ImGui::Button("Save As...")) {
306 if (!path.empty()) {
309 }
310 }
311
312 ImGui::SameLine();
313 if (ImGui::Button("Export to JSON")) {
315 if (!path.empty()) {
317 }
318 }
319 }
320
321 EndChild();
322}
323
325 ImGui::BeginChild("##TextCommands",
326 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
327 ImGuiWindowFlags_AlwaysVerticalScrollbar);
328 static uint8_t command_parameter = 0;
329 gui::InputHexByte("Command Parameter", &command_parameter);
330 for (const auto& text_element : TextCommands) {
331 if (Button(text_element.GenericToken.c_str())) {
332 message_text_box_.text.append(
333 text_element.GetParamToken(command_parameter));
334 }
335 SameLine();
336 TextWrapped("%s", text_element.Description.c_str());
337 Separator();
338 }
339 EndChild();
340}
341
343 ImGui::BeginChild("##SpecialChars",
344 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
345 ImGuiWindowFlags_AlwaysVerticalScrollbar);
346 for (const auto& text_element : SpecialChars) {
347 if (Button(text_element.GenericToken.c_str())) {
348 message_text_box_.text.append(text_element.GenericToken);
349 }
350 SameLine();
351 TextWrapped("%s", text_element.Description.c_str());
352 Separator();
353 }
354 EndChild();
355}
356
358 if (ImGui::BeginChild("##DictionaryChild",
359 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
360 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
361 if (BeginTable("##Dictionary", 2, kMessageTableFlags)) {
362 TableSetupColumn("ID");
363 TableSetupColumn("Contents");
364 TableHeadersRow();
365 for (const auto& dictionary : message_preview_.all_dictionaries_) {
366 TableNextColumn();
367 Text("%s", util::HexWord(dictionary.ID).c_str());
368 TableNextColumn();
369 Text("%s", dictionary.Contents.c_str());
370 }
371 EndTable();
372 }
373 }
374 EndChild();
375}
376
378 // Render the message to the preview bitmap
380
381 // Validate preview data before updating
383 LOG_WARN("MessageEditor", "Preview data is empty, skipping bitmap update");
384 return;
385 }
386
388 // CRITICAL: Use set_data() to properly update both data_ AND surface_
389 // mutable_data() returns a reference but doesn't update the surface!
391
392 // Validate surface was updated
394 LOG_ERROR("MessageEditor", "Bitmap surface is null after set_data()");
395 return;
396 }
397
398 // Queue texture update so changes are visible immediately
401
402 LOG_DEBUG(
403 "MessageEditor",
404 "Updated message preview bitmap (size: %zu) and queued texture update",
406 } else {
407 // Create bitmap and queue texture creation with 8-bit indexed depth
414
415 LOG_INFO("MessageEditor",
416 "Created message preview bitmap (%dx%d) with 8-bit depth and "
417 "queued texture creation",
419 }
420}
421
422absl::Status MessageEditor::Save() {
423 std::vector<uint8_t> backup = rom()->vector();
424
425 for (int i = 0; i < kWidthArraySize; i++) {
426 RETURN_IF_ERROR(rom()->WriteByte(kCharactersWidth + i,
428 }
429
430 int pos = kTextData;
431 bool in_second_bank = false;
432
433 for (const auto& message : list_of_texts_) {
434 for (const auto value : message.Data) {
435 RETURN_IF_ERROR(rom()->WriteByte(pos, value));
436
437 if (value == kBlockTerminator) {
438 // Make sure we didn't go over the space available in the first block.
439 // 0x7FFF available.
440 if ((!in_second_bank & pos) > kTextDataEnd) {
441 return absl::InternalError(DisplayTextOverflowError(pos, true));
442 }
443
444 // Switch to the second block.
445 pos = kTextData2 - 1;
446 in_second_bank = true;
447 }
448
449 pos++;
450 }
451
452 RETURN_IF_ERROR(rom()->WriteByte(pos++, kMessageTerminator));
453 }
454
455 // Verify that we didn't go over the space available for the second block.
456 // 0x14BF available.
457 if ((in_second_bank & pos) > kTextData2End) {
458 std::copy(backup.begin(), backup.end(), rom()->mutable_data());
459 return absl::InternalError(DisplayTextOverflowError(pos, false));
460 }
461
462 RETURN_IF_ERROR(rom()->WriteByte(pos, 0xFF));
463
464 return absl::OkStatus();
465}
466
468 if (expanded_message_path_.empty()) {
469 return absl::InvalidArgumentError("No path specified for expanded messages");
470 }
471
472 // Ensure the ROM object is loaded/initialized if needed, or just use it as a buffer
473 // The original code used expanded_message_bin_ which wasn't clearly initialized in this scope
474 // except potentially in LoadExpandedMessages via a static local?
475 // Wait, LoadExpandedMessages used a static local Rom.
476 // We need to ensure expanded_message_bin_ member is populated or we load it.
477
479 // Try to load from the path if it exists, otherwise create new?
480 // For now, let's assume we are overwriting or updating.
481 // If we are just writing raw data, maybe we don't need a full ROM load if we just write bytes?
482 // But SaveToFile expects a loaded ROM structure.
483 // Let's try to load it first.
485 if (!status.ok()) {
486 // If file doesn't exist, maybe we should create a buffer?
487 // For now, let's propagate error if we can't load it to update it.
488 // Or if it's a new file, we might need to handle that.
489 // Let's assume for this task we are updating an existing BIN or creating one.
490 // If creating, we might need to initialize expanded_message_bin_ with enough size.
491 // Let's just try to load, and if it fails (e.g. new file), initialize empty.
492 expanded_message_bin_.Expand(0x200000); // Default 2MB? Or just enough?
493 }
494 }
495
496 for (const auto& expanded_message : expanded_messages_) {
497 // Ensure vector is large enough
498 if (expanded_message.Address + expanded_message.Data.size() > expanded_message_bin_.size()) {
499 expanded_message_bin_.Expand(expanded_message.Address + expanded_message.Data.size() + 0x1000);
500 }
501 std::copy(expanded_message.Data.begin(), expanded_message.Data.end(),
502 expanded_message_bin_.mutable_data() + expanded_message.Address);
503 }
504
505 // Write terminator
506 if (!expanded_messages_.empty()) {
507 size_t end_pos = expanded_messages_.back().Address + expanded_messages_.back().Data.size();
508 if (end_pos < expanded_message_bin_.size()) {
509 expanded_message_bin_.WriteByte(end_pos, 0xFF);
510 }
511 }
512
515 Rom::SaveSettings{.backup = true, .save_new = false}));
516 return absl::OkStatus();
517}
518
519absl::Status MessageEditor::Cut() {
520 // Ensure that text is currently selected in the text box.
521 if (!message_text_box_.text.empty()) {
522 // Cut the selected text in the control and paste it into the Clipboard.
524 }
525 return absl::OkStatus();
526}
527
528absl::Status MessageEditor::Paste() {
529 // Determine if there is any text in the Clipboard to paste into the
530 if (ImGui::GetClipboardText() != nullptr) {
531 // Paste the text from the Clipboard into the text box.
533 }
534 return absl::OkStatus();
535}
536
537absl::Status MessageEditor::Copy() {
538 // Ensure that text is selected in the text box.
540 // Copy the selected text to the Clipboard.
542 }
543 return absl::OkStatus();
544}
545
546absl::Status MessageEditor::Undo() {
547 // Determine if last operation can be undone in text box.
549 // Undo the last operation.
551
552 // clear the undo buffer to prevent last action from being redone.
554 }
555 return absl::OkStatus();
556}
557
558absl::Status MessageEditor::Redo() {
559 // Implementation of redo functionality
560 // This would require tracking a redo stack in the TextBox struct
561 return absl::OkStatus();
562}
563
565 // Determine if any text is selected in the TextBox control.
567 // clear all of the text in the textbox.
569 }
570}
571
573 // Determine if any text is selected in the TextBox control.
575 // Select all text in the text box.
577
578 // Move the cursor to the text box.
580 }
581}
582
583absl::Status MessageEditor::Find() {
584 if (ImGui::Begin("Find Text", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
585 static char find_text[256] = "";
586 ImGui::InputText("Search", find_text, IM_ARRAYSIZE(find_text));
587
588 if (ImGui::Button("Find Next")) {
589 search_text_ = find_text;
590 }
591
592 ImGui::SameLine();
593 if (ImGui::Button("Find All")) {
594 search_text_ = find_text;
595 }
596
597 ImGui::SameLine();
598 if (ImGui::Button("Replace")) {
599 // TODO: Implement replace functionality
600 }
601
602 ImGui::Checkbox("Case Sensitive", &case_sensitive_);
603 ImGui::SameLine();
604 ImGui::Checkbox("Match Whole Word", &match_whole_word_);
605 }
606 ImGui::End();
607
608 return absl::OkStatus();
609}
610
611} // namespace editor
612} // namespace yaze
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:74
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:286
auto mutable_data()
Definition rom.h:136
const auto & vector() const
Definition rom.h:139
void Expand(int size)
Definition rom.h:49
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:164
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
auto set_filename(std::string_view name)
Definition rom.h:142
bool is_loaded() const
Definition rom.h:128
zelda3::GameData * game_data() const
Definition editor.h:228
EditorDependencies dependencies_
Definition editor.h:237
std::vector< std::string > parsed_messages_
absl::Status Copy() override
std::vector< MessageData > expanded_messages_
absl::Status Find() override
absl::Status Update() override
absl::Status Paste() override
absl::Status Undo() override
absl::Status Load() override
std::array< uint8_t, 0x4000 > raw_font_gfx_data_
absl::Status Cut() override
std::vector< MessageData > list_of_texts_
gfx::SnesPalette font_preview_colors_
absl::Status Redo() override
absl::Status Save() override
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:34
static Arena & Get()
Definition arena.cc:19
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:199
bool is_active() const
Definition bitmap.h:384
SnesPalette * mutable_palette()
Definition bitmap.h:369
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:851
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:382
SDL_Surface * surface() const
Definition bitmap.h:379
RAII timer for automatic timing management.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1075
void DrawContextMenu()
Definition canvas.cc:602
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
auto canvas_size() const
Definition canvas.h:451
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:547
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1398
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
std::string DisplayTextOverflowError(int pos, bool bank)
constexpr int kCharactersWidth
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos)
absl::Status LoadExpandedMessages(std::string &expanded_message_path, std::vector< std::string > &parsed_messages, std::vector< MessageData > &expanded_messages, std::vector< DictionaryEntry > &dictionary)
constexpr int kTextData
constexpr int kCurrentMessageWidth
constexpr int kTextData2
constexpr int kCurrentMessageHeight
constexpr uint8_t kBlockTerminator
constexpr int kGfxFont
constexpr int kFontGfxMessageSize
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
constexpr uint8_t kMessageTerminator
constexpr int kFontGfxMessageDepth
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
std::vector< uint8_t > ParseMessageToData(std::string str)
absl::Status ExportMessagesToJson(const std::string &path, const std::vector< MessageData > &messages)
constexpr uint8_t kWidthArraySize
constexpr ImGuiTableFlags kMessageTableFlags
constexpr int kTextDataEnd
std::vector< uint8_t > SnesTo8bppSheet(std::span< uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:131
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1509
void BeginPadding(int i)
Definition style.cc:274
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1486
void EndNoPadding()
Definition style.cc:286
void MemoryEditorPopup(const std::string &label, std::span< uint8_t > memory)
Definition input.cc:689
void EndPadding()
Definition style.cc:278
void BeginNoPadding()
Definition style.cc:282
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
Definition color.cc:235
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:370
std::string HexWord(uint16_t word, HexStringParams params)
Definition hex.cc:41
std::string HexLong(uint32_t dword, HexStringParams params)
Definition hex.cc:52
absl::StatusOr< gfx::Bitmap > LoadFontGraphics(const Rom &rom)
Loads font graphics from ROM.
Definition game_data.cc:493
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< uint8_t > Data
std::vector< uint8_t > current_preview_data_
void DrawMessagePreview(const MessageData &message)
std::array< uint8_t, kWidthArraySize > width_array
std::vector< uint8_t > font_gfx16_data_2_
std::vector< uint8_t > font_gfx16_data_
std::vector< DictionaryEntry > all_dictionaries_
auto palette(int i) const
void SelectAll()
Definition style.h:102
std::string text
Definition style.h:57
void clearUndo()
Definition style.h:73
int selection_length
Definition style.h:62
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89