yaze 0.2.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 <unordered_map>
5#include <vector>
6
7#include "absl/status/status.h"
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_replace.h"
12#include "app/gfx/bitmap.h"
14#include "app/gfx/snes_tile.h"
15#include "app/gui/canvas.h"
16#include "app/gui/style.h"
17#include "app/rom.h"
18#include "imgui.h"
19#include "imgui/misc/cpp/imgui_stdlib.h"
20#include "util/hex.h"
21
22namespace yaze {
23namespace editor {
24
25using core::Renderer;
26
27using ImGui::BeginChild;
28using ImGui::BeginTable;
29using ImGui::Button;
30using ImGui::EndChild;
31using ImGui::EndTable;
32using ImGui::InputTextMultiline;
33using ImGui::SameLine;
34using ImGui::Separator;
35using ImGui::TableHeadersRow;
36using ImGui::TableNextColumn;
37using ImGui::TableSetupColumn;
38using ImGui::Text;
39using ImGui::TextWrapped;
40
41constexpr ImGuiTableFlags kMessageTableFlags = ImGuiTableFlags_Hideable |
42 ImGuiTableFlags_Borders |
43 ImGuiTableFlags_Resizable;
44
46 for (int i = 0; i < kWidthArraySize; i++) {
47 width_array[i] = rom()->data()[kCharactersWidth + i];
48 }
49
52
53 font_preview_colors_.AddColor(gfx::SnesColor(0x7FFF)); // White
54 font_preview_colors_.AddColor(gfx::SnesColor(0x7C00)); // Red
55 font_preview_colors_.AddColor(gfx::SnesColor(0x03E0)); // Green
56 font_preview_colors_.AddColor(gfx::SnesColor(0x001F)); // Blue
57
58 std::vector<uint8_t> data(0x4000, 0);
59 for (int i = 0; i < 0x4000; i++) {
60 data[i] = rom()->data()[kGfxFont + i];
61 }
62 font_gfx16_data_ = gfx::SnesTo8bppSheet(data, /*bpp=*/2, /*num_sheets=*/2);
63
64 // 4bpp
68
71 for (int i = 0; i < kCurrentMessageWidth * kCurrentMessageHeight; i++) {
72 current_font_gfx16_data_.push_back(0);
73 }
74
75 // 8bpp
79
80 *font_gfx_bitmap_.mutable_palette() = font_preview_colors_;
82
85}
86
87absl::Status MessageEditor::Load() { return absl::OkStatus(); }
88
89absl::Status MessageEditor::Update() {
90 if (rom()->is_loaded() && !data_loaded_) {
91 Initialize();
93 data_loaded_ = true;
94 }
95
96 if (BeginTable("##MessageEditor", 4, kMessageTableFlags)) {
97 TableSetupColumn("List");
98 TableSetupColumn("Contents");
99 TableSetupColumn("Commands");
100 TableSetupColumn("Dictionary");
101
102 TableHeadersRow();
103
104 TableNextColumn();
106
107 TableNextColumn();
109
110 TableNextColumn();
113
114 TableNextColumn();
117
118 EndTable();
119 }
121 return absl::OkStatus();
122}
123
125 if (BeginChild("##MessagesList", ImVec2(0, 0), true,
126 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
127 if (BeginTable("##MessagesTable", 3, kMessageTableFlags)) {
128 TableSetupColumn("ID");
129 TableSetupColumn("Contents");
130 TableSetupColumn("Data");
131
132 TableHeadersRow();
133 for (const auto& message : list_of_texts_) {
134 TableNextColumn();
135 if (Button(util::HexWord(message.ID).c_str())) {
136 current_message_ = message;
138 }
139 TableNextColumn();
140 TextWrapped("%s", parsed_messages_[message.ID].c_str());
141 TableNextColumn();
142 TextWrapped("%s",
143 util::HexLong(list_of_texts_[message.ID].Address).c_str());
144 }
145
146 EndTable();
147 }
148 }
149 EndChild();
150}
151
153 if (!rom()->is_loaded()) {
154 return;
155 }
156 Button(absl::StrCat("Message ", current_message_.ID).c_str());
157 if (InputTextMultiline("##MessageEditor",
159 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
162 }
163 Separator();
164
165 Text("Font Graphics");
167 BeginChild("MessageEditorCanvas", ImVec2(0, 130));
168 font_gfx_canvas_.DrawBackground();
169 font_gfx_canvas_.DrawContextMenu();
170 font_gfx_canvas_.DrawBitmap(font_gfx_bitmap_, 0, 0);
171 font_gfx_canvas_.DrawGrid();
172 font_gfx_canvas_.DrawOverlay();
173 EndChild();
175 Separator();
176
177 Text("Message Preview");
178 if (Button("Create Preview")) {
180 }
181 if (Button("Refresh Bitmap")) {
183 }
184 ImGui::SameLine();
185 if (Button("View Palette")) {
186 ImGui::OpenPopup("Palette");
187 }
188 if (ImGui::BeginPopup("Palette")) {
190 ImGui::EndPopup();
191 }
193 BeginChild("CurrentGfxFont", ImVec2(current_font_gfx16_bitmap_.width(), 0),
194 true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
195 current_font_gfx16_canvas_.DrawBackground();
197 current_font_gfx16_canvas_.DrawContextMenu();
200 current_font_gfx16_canvas_.DrawOverlay();
201 EndChild();
202}
203
205 ImGui::BeginChild("##TextCommands",
206 ImVec2(0, ImGui::GetWindowContentRegionMax().y / 2), true,
207 ImGuiWindowFlags_AlwaysVerticalScrollbar);
208 for (const auto& text_element : TextCommands) {
209 if (Button(text_element.GenericToken.c_str())) {
210 message_text_box_.text.append(text_element.GenericToken);
211 }
212 SameLine();
213 TextWrapped("%s", text_element.Description.c_str());
214 Separator();
215 }
216 EndChild();
217}
218
220 ImGui::BeginChild("##SpecialChars",
221 ImVec2(0, ImGui::GetWindowContentRegionMax().y / 2), true,
222 ImGuiWindowFlags_AlwaysVerticalScrollbar);
223 for (const auto& text_element : SpecialChars) {
224 if (Button(text_element.GenericToken.c_str())) {
225 message_text_box_.text.append(text_element.GenericToken);
226 }
227 Separator();
228 }
229 EndChild();
230}
231
233 if (all_dictionaries_.empty()) {
234 return;
235 }
236 if (ImGui::BeginChild("##DictionaryChild",
237 ImVec2(200, ImGui::GetWindowContentRegionMax().y / 2),
238 true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
239 if (BeginTable("##Dictionary", 2, kMessageTableFlags)) {
240 TableSetupColumn("ID");
241 TableSetupColumn("Contents");
242 TableHeadersRow();
243 for (const auto& dictionary : all_dictionaries_) {
244 TableNextColumn();
245 Text("%s", util::HexWord(dictionary.ID).c_str());
246 TableNextColumn();
247 Text("%s", dictionary.Contents.c_str());
248 }
249 EndTable();
250 }
251 }
252 EndChild();
253}
254
256 ImGui::Text("Import Messages");
257 ImGui::Separator();
258
259 static char import_path[256] = "";
260 ImGui::InputText("Import File", import_path, sizeof(import_path));
261
262 if (ImGui::Button("Import")) {
263 status_ = ImportMessagesFromFile(import_path);
264 }
265
266 // Export section
267 ImGui::Spacing();
268 ImGui::Text("Export Messages");
269 ImGui::Separator();
270
271 static char export_path[256] = "";
272 ImGui::InputText("Export File", export_path, sizeof(export_path));
273
274 if (ImGui::Button("Export")) {
275 status_ = ExportMessagesToFile(export_path);
276 }
277}
278
279void MessageEditor::DrawTileToPreview(int x, int y, int srcx, int srcy, int pal,
280 int sizex, int sizey) {
281 const int num_x_tiles = 16;
282 const int img_width = 512; // (imgwidth/2)
283 int draw_id = srcx + (srcy * 32);
284 for (int yl = 0; yl < sizey * 8; yl++) {
285 for (int xl = 0; xl < 4; xl++) {
286 int mx = xl;
287 int my = yl;
288
289 // Formula information to get tile index position in the array.
290 // ((ID / nbrofXtiles) * (imgwidth/2) + (ID - ((ID/16)*16) ))
291 int tx = ((draw_id / num_x_tiles) * img_width) +
292 ((draw_id - ((draw_id / 16) * 16)) * 4);
293 uint8_t pixel = font_gfx16_data_[tx + (yl * 64) + xl];
294
295 // nx,ny = object position, xx,yy = tile position, xl,yl = pixel
296 // position
297 int index = x + (y * 172) + (mx * 2) + (my * 172);
298 if ((pixel & 0x0F) != 0) {
299 current_font_gfx16_data_[index + 1] =
300 (uint8_t)((pixel & 0x0F) + (0 * 4));
301 }
302
303 if (((pixel >> 4) & 0x0F) != 0) {
304 current_font_gfx16_data_[index + 0] =
305 (uint8_t)(((pixel >> 4) & 0x0F) + (0 * 4));
306 }
307 }
308 }
309}
310
312 for (const auto c : str) {
314 }
315}
316
320
321void MessageEditor::DrawCharacterToPreview(const std::vector<uint8_t>& text) {
322 for (const uint8_t& value : text) {
323 if (skip_next) {
324 skip_next = false;
325 continue;
326 }
327
328 if (value < 100) {
329 int srcy = value / 16;
330 int srcx = value - (value & (~0xF));
331
332 if (text_position_ >= 170) {
333 text_position_ = 0;
334 text_line_++;
335 }
336
337 DrawTileToPreview(text_position_, text_line_ * 16, srcx, srcy, 0, 1, 2);
338 text_position_ += width_array[value];
339 } else if (value == kLine1) {
340 text_position_ = 0;
341 text_line_ = 0;
342 } else if (value == kScrollVertical) {
343 text_position_ = 0;
344 text_line_ += 1;
345 } else if (value == kLine2) {
346 text_position_ = 0;
347 text_line_ = 1;
348 } else if (value == kLine3) {
349 text_position_ = 0;
350 text_line_ = 2;
351 } else if (value == 0x6B || value == 0x6D || value == 0x6E ||
352 value == 0x77 || value == 0x78 || value == 0x79 ||
353 value == 0x7A) {
354 skip_next = true;
355
356 continue;
357 } else if (value == 0x6C) // BCD numbers.
358 {
360 skip_next = true;
361
362 continue;
363 } else if (value == 0x6A) {
364 // Includes parentheses to be longer, since player names can be up to 6
365 // characters.
366 DrawStringToPreview("(NAME)");
367 } else if (value >= DICTOFF && value < (DICTOFF + 97)) {
368 int pos = value - DICTOFF;
369 if (pos < 0 || pos >= all_dictionaries_.size()) {
370 // Invalid dictionary entry.
371 std::cerr << "Invalid dictionary entry: " << pos << std::endl;
372 continue;
373 }
374 auto dictionary_entry = all_dictionaries_[pos];
375 DrawCharacterToPreview(dictionary_entry.Data);
376 }
377 }
378}
379
393
394absl::Status MessageEditor::Save() {
395 std::vector<uint8_t> backup = rom()->vector();
396
397 for (int i = 0; i < kWidthArraySize; i++) {
398 RETURN_IF_ERROR(rom()->WriteByte(kCharactersWidth + i, width_array[i]));
399 }
400
401 int pos = kTextData;
402 bool in_second_bank = false;
403
404 for (const auto& message : list_of_texts_) {
405 for (const auto value : message.Data) {
406 RETURN_IF_ERROR(rom()->WriteByte(pos, value));
407
408 if (value == kBlockTerminator) {
409 // Make sure we didn't go over the space available in the first block.
410 // 0x7FFF available.
411 if ((!in_second_bank & pos) > kTextDataEnd) {
412 return absl::InternalError(DisplayTextOverflowError(pos, true));
413 }
414
415 // Switch to the second block.
416 pos = kTextData2 - 1;
417 in_second_bank = true;
418 }
419
420 pos++;
421 }
422
423 RETURN_IF_ERROR(rom()->WriteByte(pos++, kMessageTerminator));
424 }
425
426 // Verify that we didn't go over the space available for the second block.
427 // 0x14BF available.
428 if ((in_second_bank & pos) > kTextData2End) {
429 // TODO: Restore the backup.
430 return absl::InternalError(DisplayTextOverflowError(pos, false));
431 }
432
433 RETURN_IF_ERROR(rom()->WriteByte(pos, 0xFF));
434
435 return absl::OkStatus();
436}
437
438std::string MessageEditor::DisplayTextOverflowError(int pos, bool bank) {
439 int space = bank ? kTextDataEnd - kTextData : kTextData2End - kTextData2;
440 std::string bankSTR = bank ? "1st" : "2nd";
441 std::string posSTR =
442 bank ? absl::StrFormat("%X4", pos & 0xFFFF)
443 : absl::StrFormat("%X4", (pos - kTextData2) & 0xFFFF);
444 std::string message = absl::StrFormat(
445 "There is too much text data in the %s block to save.\n"
446 "Available: %X4 | Used: %s",
447 bankSTR, space, posSTR);
448 return message;
449}
450
451absl::Status MessageEditor::Cut() {
452 // Ensure that text is currently selected in the text box.
453 if (!message_text_box_.text.empty()) {
454 // Cut the selected text in the control and paste it into the Clipboard.
455 message_text_box_.Cut();
456 }
457 return absl::OkStatus();
458}
459
460absl::Status MessageEditor::Paste() {
461 // Determine if there is any text in the Clipboard to paste into the
462 if (ImGui::GetClipboardText() != nullptr) {
463 // Paste the text from the Clipboard into the text box.
464 message_text_box_.Paste();
465 }
466 return absl::OkStatus();
467}
468
469absl::Status MessageEditor::Copy() {
470 // Ensure that text is selected in the text box.
471 if (message_text_box_.selection_length > 0) {
472 // Copy the selected text to the Clipboard.
473 message_text_box_.Copy();
474 }
475 return absl::OkStatus();
476}
477
478absl::Status MessageEditor::Undo() {
479 // Determine if last operation can be undone in text box.
480 if (message_text_box_.can_undo) {
481 // Undo the last operation.
482 message_text_box_.Undo();
483
484 // clear the undo buffer to prevent last action from being redone.
485 message_text_box_.clearUndo();
486 }
487 return absl::OkStatus();
488}
489
490absl::Status MessageEditor::Redo() {
491 // Implementation of redo functionality
492 // This would require tracking a redo stack in the TextBox struct
493 return absl::OkStatus();
494}
495
497 // Determine if any text is selected in the TextBox control.
498 if (message_text_box_.selection_length == 0) {
499 // clear all of the text in the textbox.
500 message_text_box_.clear();
501 }
502}
503
505 // Determine if any text is selected in the TextBox control.
506 if (message_text_box_.selection_length == 0) {
507 // Select all text in the text box.
508 message_text_box_.SelectAll();
509
510 // Move the cursor to the text box.
511 message_text_box_.Focus();
512 }
513}
514
515absl::Status MessageEditor::Find() {
516 if (ImGui::Begin("Find Text", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
517 static char find_text[256] = "";
518 ImGui::InputText("Search", find_text, IM_ARRAYSIZE(find_text));
519
520 if (ImGui::Button("Find Next")) {
521 search_text_ = find_text;
522 }
523
524 ImGui::SameLine();
525 if (ImGui::Button("Find All")) {
526 search_text_ = find_text;
527 }
528
529 ImGui::SameLine();
530 if (ImGui::Button("Replace")) {
531 // TODO: Implement replace functionality
532 }
533
534 ImGui::Checkbox("Case Sensitive", &case_sensitive_);
535 ImGui::SameLine();
536 ImGui::Checkbox("Match Whole Word", &match_whole_word_);
537 }
538 ImGui::End();
539
540 return absl::OkStatus();
541}
542
543absl::Status MessageEditor::ImportMessagesFromFile(const std::string& path) {
544 // Open the file
545 std::ifstream file(path);
546 if (!file.is_open()) {
547 return absl::NotFoundError("Failed to open file");
548 }
549
550 // Read the file line by line
551 std::string line;
552 int line_number = 0;
553
554 while (std::getline(file, line)) {
555 line_number++;
556
557 // Skip empty lines and comments
558 if (line.empty() || line[0] == '#') {
559 continue;
560 }
561
562 // Parse the line
563 // Format: ID=content
564 size_t equal_pos = line.find('=');
565 if (equal_pos == std::string::npos) {
566 return absl::InvalidArgumentError(
567 absl::StrFormat("Invalid format at line %d", line_number));
568 }
569
570 std::string id_str = line.substr(0, equal_pos);
571 std::string content = line.substr(equal_pos + 1);
572
573 // Parse the ID
574 int id;
575 if (!absl::SimpleAtoi(id_str, &id)) {
576 return absl::InvalidArgumentError(
577 absl::StrFormat("Invalid ID at line %d", line_number));
578 }
579
580 // Update a regular message
581 for (auto& message : list_of_texts_) {
582 if (message.ID == id) {
583 message.ContentsParsed = content;
584 message.DataParsed = ParseMessageToData(content);
585 break;
586 }
587 }
588 }
589
590 return absl::OkStatus();
591}
592
593absl::Status MessageEditor::ExportMessagesToFile(const std::string& path) {
594 // Open the file
595 std::ofstream file(path);
596 if (!file.is_open()) {
597 return absl::NotFoundError("Failed to open file");
598 }
599
600 // Write a header
601 file << "# Message Export\n";
602 file << "# Format: ID=content\n\n";
603
604 // Write regular messages
605 for (const auto& message : list_of_texts_) {
606 file << absl::StrFormat("%d=%s\n", message.ID, message.ContentsParsed);
607 }
608
609 return absl::OkStatus();
610}
611
612} // namespace editor
613} // namespace yaze
auto vector() const
Definition rom.h:168
auto data() const
Definition rom.h:163
void CreateAndRenderBitmap(int width, int height, int depth, const std::vector< uint8_t > &data, gfx::Bitmap &bitmap, gfx::SnesPalette &palette)
Definition renderer.h:59
void UpdateBitmap(gfx::Bitmap *bitmap)
Used to update a bitmap on the screen.
Definition renderer.h:55
void RenderBitmap(gfx::Bitmap *bitmap)
Used to render a bitmap to the screen.
Definition renderer.h:48
std::vector< std::string > parsed_messages_
absl::Status Copy() override
absl::Status Find() override
absl::Status Update() override
std::array< uint8_t, kWidthArraySize > width_array
absl::Status Paste() override
std::string DisplayTextOverflowError(int pos, bool bank)
void DrawStringToPreview(std::string str)
absl::Status Undo() override
absl::Status Load() override
void DrawTileToPreview(int x, int y, int srcx, int srcy, int pal, int sizex=1, int sizey=1)
absl::Status ExportMessagesToFile(const std::string &filename)
absl::Status Cut() override
std::vector< MessageData > list_of_texts_
std::vector< uint8_t > font_gfx16_data_
std::vector< uint8_t > current_font_gfx16_data_
gfx::SnesPalette font_preview_colors_
absl::Status Redo() override
std::vector< DictionaryEntry > all_dictionaries_
absl::Status Save() override
absl::Status ImportMessagesFromFile(const std::string &filename)
static Renderer & GetInstance()
Definition renderer.h:26
SNES Color container.
Definition snes_color.h:38
#define RETURN_IF_ERROR(expression)
Definition macro.h:51
#define CLEAR_AND_RETURN_STATUS(status)
Definition macro.h:94
Editors are the view controllers for the application.
constexpr int kCharactersWidth
uint8_t FindMatchingCharacter(char value)
const uint8_t kMessageTerminator
constexpr uint8_t kScrollVertical
constexpr int kFontGfx16Size
constexpr uint8_t kLine1
constexpr int kTextData
constexpr int kCurrentMessageWidth
constexpr int kTextData2
constexpr int kCurrentMessageHeight
constexpr uint8_t kBlockTerminator
constexpr uint8_t kLine2
constexpr int kGfxFont
constexpr int kFontGfxMessageSize
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
constexpr int kFontGfxMessageDepth
void ReadAllTextData(Rom *rom, std::vector< MessageData > &list_of_texts_)
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
std::vector< uint8_t > ParseMessageToData(std::string str)
constexpr uint8_t kWidthArraySize
constexpr uint8_t DICTOFF
constexpr ImGuiTableFlags kMessageTableFlags
constexpr uint8_t kLine3
constexpr int kTextDataEnd
std::vector< uint8_t > SnesTo8bppSheet(const std::vector< uint8_t > &sheet, int bpp, int num_sheets)
Definition snes_tile.cc:139
void BeginPadding(int i)
Definition style.cc:372
void EndPadding()
Definition style.cc:376
absl::Status DisplayPalette(gfx::SnesPalette &palette, bool loaded)
Definition color.cc:54
std::string HexWord(uint16_t word, HexStringParams params)
Definition hex.cc:46
std::string HexLong(uint32_t dword, HexStringParams params)
Definition hex.cc:59
Main namespace for the application.
Definition controller.cc:18