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 "imgui.h"
20#include "imgui/misc/cpp/imgui_stdlib.h"
21#include "rom/rom.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(
74 std::make_unique<MessageListPanel>([this]() { DrawMessageList(); }));
75 panel_manager->RegisterEditorPanel(
76 std::make_unique<MessageEditorPanel>([this]() { DrawCurrentMessage(); }));
77 panel_manager->RegisterEditorPanel(std::make_unique<FontAtlasPanel>([this]() {
80 }));
81 panel_manager->RegisterEditorPanel(
82 std::make_unique<DictionaryPanel>([this]() {
86 }));
87
88 // Show message list by default
89 panel_manager->ShowPanel(session_id, "message.message_list");
90
91 for (int i = 0; i < kWidthArraySize; i++) {
93 }
94
96 list_of_texts_ = ReadAllTextData(rom()->mutable_data());
97 LOG_INFO("MessageEditor", "Loaded %zu messages from ROM",
98 list_of_texts_.size());
99
100 if (game_data()) {
102 }
103
104 for (int i = 0; i < 0x4000; i++) {
105 raw_font_gfx_data_[i] = rom()->data()[kGfxFont + i];
106 }
108 gfx::SnesTo8bppSheet(raw_font_gfx_data_, /*bpp=*/2, /*num_sheets=*/2);
109
110 // Create bitmap for font graphics
115
116 // Queue texture creation - will be processed in render loop
119
120 LOG_INFO("MessageEditor", "Font bitmap created and texture queued");
122
123 auto load_font = zelda3::LoadFontGraphics(*rom());
124 if (load_font.ok()) {
125 message_preview_.font_gfx16_data_2_ = load_font.value().vector();
126 }
129
130 if (!list_of_texts_.empty()) {
131 // Default to message 1 if available, otherwise 0
132 size_t default_idx = list_of_texts_.size() > 1 ? 1 : 0;
133 current_message_ = list_of_texts_[default_idx];
136 } else {
137 LOG_ERROR("MessageEditor", "No messages found in ROM!");
138 }
139}
140
141absl::Status MessageEditor::Load() {
142 gfx::ScopedTimer timer("MessageEditor::Load");
143 return absl::OkStatus();
144}
145
146absl::Status MessageEditor::Update() {
147 // Panel drawing is handled centrally by PanelManager::DrawAllVisiblePanels()
148 // via the EditorPanel implementations registered in Initialize().
149 // No local drawing needed here.
150 return absl::OkStatus();
151}
152
155 if (BeginChild("##MessagesList", ImVec2(0, 0), true,
156 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
158 if (BeginTable("##MessagesTable", 3, kMessageTableFlags)) {
159 TableSetupColumn("ID");
160 TableSetupColumn("Contents");
161 TableSetupColumn("Data");
162
163 TableHeadersRow();
164 for (const auto& message : list_of_texts_) {
165 TableNextColumn();
166 PushID(message.ID);
167 if (Button(util::HexWord(message.ID).c_str())) {
168 current_message_ = message;
171 }
172 PopID();
173 TableNextColumn();
174 TextWrapped("%s", parsed_messages_[message.ID].c_str());
175 TableNextColumn();
176 TextWrapped("%s",
177 util::HexLong(list_of_texts_[message.ID].Address).c_str());
178 }
179 for (const auto& expanded_message : expanded_messages_) {
180 TableNextColumn();
181 PushID(expanded_message.ID + 0x18D);
182 if (Button(util::HexWord(expanded_message.ID + 0x18D).c_str())) {
183 current_message_ = expanded_message;
185 parsed_messages_[expanded_message.ID + 0x18D];
187 }
188 PopID();
189 TableNextColumn();
190 TextWrapped("%s",
191 parsed_messages_[expanded_message.ID + 0x18C].c_str());
192 TableNextColumn();
193 TextWrapped("%s", util::HexLong(expanded_message.Address).c_str());
194 }
195
196 EndTable();
197 }
198 }
199 EndChild();
200}
201
203 Button(absl::StrCat("Message ", current_message_.ID).c_str());
204 if (InputTextMultiline("##MessageEditor", &message_text_box_.text,
205 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
206 std::string temp = message_text_box_.text;
207 // Strip newline characters.
208 temp.erase(std::remove(temp.begin(), temp.end(), '\n'), temp.end());
211 }
212 Separator();
214
215 ImGui::BeginChild("##MessagePreview", ImVec2(0, 0), true, 1);
216 Text("Message Preview");
217 if (Button("View Palette")) {
218 ImGui::OpenPopup("Palette");
219 }
220 if (ImGui::BeginPopup("Palette")) {
222 ImGui::EndPopup();
223 }
225 BeginChild("CurrentGfxFont", ImVec2(348, 0), true,
226 ImGuiWindowFlags_NoScrollWithMouse);
230
231 // Handle mouse wheel scrolling
232 if (ImGui::IsWindowHovered()) {
233 float wheel = ImGui::GetIO().MouseWheel;
234 if (wheel > 0 && message_preview_.shown_lines > 0) {
236 } else if (wheel < 0 &&
239 }
240 }
241
242 // Draw only the visible portion of the text
244 current_font_gfx16_bitmap_, ImVec2(0, 0), // Destination position
245 ImVec2(340,
246 font_gfx_canvas_.canvas_size().y), // Destination size
247 ImVec2(0, message_preview_.shown_lines * 16), // Source position
248 ImVec2(170,
249 font_gfx_canvas_.canvas_size().y / 2) // Source size
250 );
251
254 EndChild();
255 ImGui::EndChild();
256}
257
264
266 ImGui::BeginChild("##ExpandedMessageSettings", ImVec2(0, 100), true,
267 ImGuiWindowFlags_AlwaysVerticalScrollbar);
268 ImGui::Text("Expanded Messages");
269
270 if (ImGui::Button("Load Expanded Message")) {
272 if (!path.empty()) {
277 .ok()) {
278 if (auto* popup_manager = dependencies_.popup_manager) {
279 popup_manager->Show("Error");
280 }
281 }
282 }
283 }
284
285 if (expanded_messages_.size() > 0) {
286 ImGui::Text("Expanded Path: %s", expanded_message_path_.c_str());
287 ImGui::Text("Expanded Messages: %lu", expanded_messages_.size());
288 if (ImGui::Button("Add New Message")) {
289 MessageData new_message;
290 new_message.ID = expanded_messages_.back().ID + 1;
291 new_message.Address = expanded_messages_.back().Address +
292 expanded_messages_.back().Data.size();
293 expanded_messages_.push_back(new_message);
294 }
295
296 if (ImGui::Button("Save Expanded Messages")) {
297 if (expanded_message_path_.empty()) {
299 }
300 if (!expanded_message_path_.empty()) {
302 }
303 }
304
305 ImGui::SameLine();
306 if (ImGui::Button("Save As...")) {
308 if (!path.empty()) {
311 }
312 }
313
314 ImGui::SameLine();
315 if (ImGui::Button("Export to JSON")) {
317 if (!path.empty()) {
319 }
320 }
321 }
322
323 EndChild();
324}
325
327 ImGui::BeginChild("##TextCommands",
328 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
329 ImGuiWindowFlags_AlwaysVerticalScrollbar);
330 static uint8_t command_parameter = 0;
331 gui::InputHexByte("Command Parameter", &command_parameter);
332 for (const auto& text_element : TextCommands) {
333 if (Button(text_element.GenericToken.c_str())) {
334 message_text_box_.text.append(
335 text_element.GetParamToken(command_parameter));
336 }
337 SameLine();
338 TextWrapped("%s", text_element.Description.c_str());
339 Separator();
340 }
341 EndChild();
342}
343
345 ImGui::BeginChild("##SpecialChars",
346 ImVec2(0, ImGui::GetContentRegionAvail().y / 2), true,
347 ImGuiWindowFlags_AlwaysVerticalScrollbar);
348 for (const auto& text_element : SpecialChars) {
349 if (Button(text_element.GenericToken.c_str())) {
350 message_text_box_.text.append(text_element.GenericToken);
351 }
352 SameLine();
353 TextWrapped("%s", text_element.Description.c_str());
354 Separator();
355 }
356 EndChild();
357}
358
360 if (ImGui::BeginChild("##DictionaryChild",
361 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
362 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
363 if (BeginTable("##Dictionary", 2, kMessageTableFlags)) {
364 TableSetupColumn("ID");
365 TableSetupColumn("Contents");
366 TableHeadersRow();
367 for (const auto& dictionary : message_preview_.all_dictionaries_) {
368 TableNextColumn();
369 Text("%s", util::HexWord(dictionary.ID).c_str());
370 TableNextColumn();
371 Text("%s", dictionary.Contents.c_str());
372 }
373 EndTable();
374 }
375 }
376 EndChild();
377}
378
380 // Render the message to the preview bitmap
382
383 // Validate preview data before updating
385 LOG_WARN("MessageEditor", "Preview data is empty, skipping bitmap update");
386 return;
387 }
388
390 // CRITICAL: Use set_data() to properly update both data_ AND surface_
391 // mutable_data() returns a reference but doesn't update the surface!
393
394 // Validate surface was updated
396 LOG_ERROR("MessageEditor", "Bitmap surface is null after set_data()");
397 return;
398 }
399
400 // Queue texture update so changes are visible immediately
403
404 LOG_DEBUG(
405 "MessageEditor",
406 "Updated message preview bitmap (size: %zu) and queued texture update",
408 } else {
409 // Create bitmap and queue texture creation with 8-bit indexed depth
416
417 LOG_INFO("MessageEditor",
418 "Created message preview bitmap (%dx%d) with 8-bit depth and "
419 "queued texture creation",
421 }
422}
423
424absl::Status MessageEditor::Save() {
425 std::vector<uint8_t> backup = rom()->vector();
426
427 for (int i = 0; i < kWidthArraySize; i++) {
428 RETURN_IF_ERROR(rom()->WriteByte(kCharactersWidth + i,
430 }
431
432 int pos = kTextData;
433 bool in_second_bank = false;
434
435 for (const auto& message : list_of_texts_) {
436 for (const auto value : message.Data) {
437 RETURN_IF_ERROR(rom()->WriteByte(pos, value));
438
439 if (value == kBlockTerminator) {
440 // Make sure we didn't go over the space available in the first block.
441 // 0x7FFF available.
442 if ((!in_second_bank & pos) > kTextDataEnd) {
443 return absl::InternalError(DisplayTextOverflowError(pos, true));
444 }
445
446 // Switch to the second block.
447 pos = kTextData2 - 1;
448 in_second_bank = true;
449 }
450
451 pos++;
452 }
453
454 RETURN_IF_ERROR(rom()->WriteByte(pos++, kMessageTerminator));
455 }
456
457 // Verify that we didn't go over the space available for the second block.
458 // 0x14BF available.
459 if ((in_second_bank & pos) > kTextData2End) {
460 std::copy(backup.begin(), backup.end(), rom()->mutable_data());
461 return absl::InternalError(DisplayTextOverflowError(pos, false));
462 }
463
464 RETURN_IF_ERROR(rom()->WriteByte(pos, 0xFF));
465
466 return absl::OkStatus();
467}
468
470 if (expanded_message_path_.empty()) {
471 return absl::InvalidArgumentError(
472 "No path specified for expanded messages");
473 }
474
475 // Ensure the ROM object is loaded/initialized if needed, or just use it as a buffer
476 // The original code used expanded_message_bin_ which wasn't clearly initialized in this scope
477 // except potentially in LoadExpandedMessages via a static local?
478 // Wait, LoadExpandedMessages used a static local Rom.
479 // We need to ensure expanded_message_bin_ member is populated or we load it.
480
482 // Try to load from the path if it exists, otherwise create new?
483 // For now, let's assume we are overwriting or updating.
484 // If we are just writing raw data, maybe we don't need a full ROM load if we just write bytes?
485 // But SaveToFile expects a loaded ROM structure.
486 // Let's try to load it first.
488 if (!status.ok()) {
489 // If file doesn't exist, maybe we should create a buffer?
490 // For now, let's propagate error if we can't load it to update it.
491 // Or if it's a new file, we might need to handle that.
492 // Let's assume for this task we are updating an existing BIN or creating one.
493 // If creating, we might need to initialize expanded_message_bin_ with enough size.
494 // Let's just try to load, and if it fails (e.g. new file), initialize empty.
495 expanded_message_bin_.Expand(0x200000); // Default 2MB? Or just enough?
496 }
497 }
498
499 for (const auto& expanded_message : expanded_messages_) {
500 // Ensure vector is large enough
501 if (expanded_message.Address + expanded_message.Data.size() >
503 expanded_message_bin_.Expand(expanded_message.Address +
504 expanded_message.Data.size() + 0x1000);
505 }
506 std::copy(expanded_message.Data.begin(), expanded_message.Data.end(),
507 expanded_message_bin_.mutable_data() + expanded_message.Address);
508 }
509
510 // Write terminator
511 if (!expanded_messages_.empty()) {
512 size_t end_pos = expanded_messages_.back().Address +
513 expanded_messages_.back().Data.size();
514 if (end_pos < expanded_message_bin_.size()) {
515 expanded_message_bin_.WriteByte(end_pos, 0xFF);
516 }
517 }
518
521 Rom::SaveSettings{.backup = true, .save_new = false}));
522 return absl::OkStatus();
523}
524
525absl::Status MessageEditor::Cut() {
526 // Ensure that text is currently selected in the text box.
527 if (!message_text_box_.text.empty()) {
528 // Cut the selected text in the control and paste it into the Clipboard.
530 }
531 return absl::OkStatus();
532}
533
534absl::Status MessageEditor::Paste() {
535 // Determine if there is any text in the Clipboard to paste into the
536 if (ImGui::GetClipboardText() != nullptr) {
537 // Paste the text from the Clipboard into the text box.
539 }
540 return absl::OkStatus();
541}
542
543absl::Status MessageEditor::Copy() {
544 // Ensure that text is selected in the text box.
546 // Copy the selected text to the Clipboard.
548 }
549 return absl::OkStatus();
550}
551
552absl::Status MessageEditor::Undo() {
553 // Determine if last operation can be undone in text box.
555 // Undo the last operation.
557
558 // clear the undo buffer to prevent last action from being redone.
560 }
561 return absl::OkStatus();
562}
563
564absl::Status MessageEditor::Redo() {
565 // Implementation of redo functionality
566 // This would require tracking a redo stack in the TextBox struct
567 return absl::OkStatus();
568}
569
571 // Determine if any text is selected in the TextBox control.
573 // clear all of the text in the textbox.
575 }
576}
577
579 // Determine if any text is selected in the TextBox control.
581 // Select all text in the text box.
583
584 // Move the cursor to the text box.
586 }
587}
588
589absl::Status MessageEditor::Find() {
590 if (ImGui::Begin("Find Text", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
591 static char find_text[256] = "";
592 ImGui::InputText("Search", find_text, IM_ARRAYSIZE(find_text));
593
594 if (ImGui::Button("Find Next")) {
595 search_text_ = find_text;
596 }
597
598 ImGui::SameLine();
599 if (ImGui::Button("Find All")) {
600 search_text_ = find_text;
601 }
602
603 ImGui::SameLine();
604 if (ImGui::Button("Replace")) {
605 // TODO: Implement replace functionality
606 }
607
608 ImGui::Checkbox("Case Sensitive", &case_sensitive_);
609 ImGui::SameLine();
610 ImGui::Checkbox("Match Whole Word", &match_whole_word_);
611 }
612 ImGui::End();
613
614 return absl::OkStatus();
615}
616
617} // namespace editor
618} // namespace yaze
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:75
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:290
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:165
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:35
static Arena & Get()
Definition arena.cc:20
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
bool is_active() const
Definition bitmap.h:384
SnesPalette * mutable_palette()
Definition bitmap.h:369
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:853
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
SDL_Surface * surface() const
Definition bitmap.h:379
RAII timer for automatic timing management.
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:132
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:236
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:561
#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