yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_data.cc
Go to the documentation of this file.
1#include "message_data.h"
2
3#include <algorithm>
4#include <cctype>
5#include <fstream>
6#include <optional>
7#include <sstream>
8#include <string>
9#include <utility>
10
11#include "absl/strings/ascii.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
14#include "core/rom_settings.h"
15#include "rom/snes.h"
16#include "rom/transaction.h"
17#include "rom/write_fence.h"
18#include "util/log.h"
19#include "util/macro.h"
20
21namespace yaze {
22namespace editor {
23
24namespace {
25
26bool IsWordChar(char c) {
27 const unsigned char uc = static_cast<unsigned char>(c);
28 return std::isalnum(uc) || c == '_';
29}
30
31bool MatchesWholeWordAt(std::string_view text, size_t pos, size_t len) {
32 const bool left_boundary = (pos == 0) || !IsWordChar(text[pos - 1]);
33 const size_t right_index = pos + len;
34 const bool right_boundary =
35 (right_index >= text.size()) || !IsWordChar(text[right_index]);
36 return left_boundary && right_boundary;
37}
38
39std::string LowercaseCopy(std::string_view input) {
40 std::string lowered(input);
41 for (char& c : lowered) {
42 c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
43 }
44 return lowered;
45}
46
47} // namespace
48
53
58
59uint8_t FindMatchingCharacter(char value) {
60 // CharEncoder contains duplicate glyph mappings (for example, space), so we
61 // choose the lowest byte value to keep reverse lookups deterministic.
62 uint8_t best_match = 0xFF;
63 const wchar_t target =
64 static_cast<wchar_t>(static_cast<unsigned char>(value));
65 for (const auto& [key, char_value] : CharEncoder) {
66 if (char_value != target) {
67 continue;
68 }
69 if (best_match == 0xFF || key < best_match) {
70 best_match = key;
71 }
72 }
73 return best_match;
74}
75
76int8_t FindDictionaryEntry(uint8_t value) {
77 if (value < DICTOFF || value == 0xFF) {
78 return -1;
79 }
80 return value - DICTOFF;
81}
82
83std::optional<TextElement> FindMatchingCommand(uint8_t b) {
84 for (const auto& text_element : TextCommands) {
85 if (text_element.ID == b) {
86 return text_element;
87 }
88 }
89 return std::nullopt;
90}
91
92std::optional<TextElement> FindMatchingSpecial(uint8_t value) {
93 auto it = std::ranges::find_if(SpecialChars,
94 [value](const TextElement& text_element) {
95 return text_element.ID == value;
96 });
97 if (it != SpecialChars.end()) {
98 return *it;
99 }
100 return std::nullopt;
101}
102
103ParsedElement FindMatchingElement(const std::string& str) {
104 std::smatch match;
105 std::vector<TextElement> commands_and_chars = TextCommands;
106 commands_and_chars.insert(commands_and_chars.end(), SpecialChars.begin(),
107 SpecialChars.end());
108 for (auto& text_element : commands_and_chars) {
109 match = text_element.MatchMe(str);
110 if (match.size() > 0) {
111 if (text_element.HasArgument) {
112 std::string arg = match[1].str().substr(1);
113 try {
114 return ParsedElement(text_element, std::stoi(arg, nullptr, 16));
115 } catch (const std::invalid_argument& e) {
116 util::logf("Error parsing argument for %s: %s",
117 text_element.GenericToken.c_str(), arg.c_str());
118 return ParsedElement(text_element, 0);
119 } catch (const std::out_of_range& e) {
120 util::logf("Argument out of range for %s: %s",
121 text_element.GenericToken.c_str(), arg.c_str());
122 return ParsedElement(text_element, 0);
123 }
124 } else {
125 return ParsedElement(text_element, 0);
126 }
127 }
128 }
129
130 const auto dictionary_element =
131 TextElement(0x80, DICTIONARYTOKEN, true, "Dictionary");
132
133 // Keep legacy bundles with assembler-style [D:$XX] tokens importable while
134 // emitting the canonical [D:XX] form. Restrict this compatibility syntax to
135 // dictionary tokens so command argument validation remains unchanged.
136 const std::regex dictionary_pattern(
137 absl::StrFormat("^\\[%s:\\$?([0-9A-F]{1,2})\\]$", DICTIONARYTOKEN));
138 std::regex_match(str, match, dictionary_pattern);
139 if (match.size() > 0) {
140 try {
141 std::string dict_arg = match[1].str();
142 const int dictionary_index = std::stoi(dict_arg, nullptr, 16);
143 if (dictionary_index < 0 || dictionary_index >= kNumDictionaryEntries) {
144 util::logf("Dictionary index out of range: %s", dict_arg.c_str());
145 return ParsedElement();
146 }
147 return ParsedElement(dictionary_element, DICTOFF + dictionary_index);
148 } catch (const std::exception& e) {
149 util::logf("Error parsing dictionary token: %s", match[1].str().c_str());
150 return ParsedElement();
151 }
152 }
153 return ParsedElement();
154}
155
156std::string ParseTextDataByte(uint8_t value) {
157 if (CharEncoder.contains(value)) {
158 char c = CharEncoder.at(value);
159 std::string str = "";
160 str.push_back(c);
161 return str;
162 }
163
164 // Check for command.
165 if (auto text_element = FindMatchingCommand(value);
166 text_element != std::nullopt) {
167 return text_element->GenericToken;
168 }
169
170 // Check for special characters.
171 if (auto special_element = FindMatchingSpecial(value);
172 special_element != std::nullopt) {
173 return special_element->GenericToken;
174 }
175
176 // Check for dictionary.
177 int8_t dictionary = FindDictionaryEntry(value);
178 if (dictionary >= 0) {
179 return absl::StrFormat("[%s:%02X]", DICTIONARYTOKEN,
180 static_cast<unsigned char>(dictionary));
181 }
182
183 return "";
184}
185
186std::vector<uint8_t> ParseMessageToData(std::string str) {
187 std::vector<uint8_t> bytes;
188 std::string temp_string = std::move(str);
189 int pos = 0;
190 while (pos < temp_string.size()) {
191 // Get next text fragment.
192 if (temp_string[pos] == '[') {
193 int next = temp_string.find(']', pos);
194 if (next == -1) {
195 break;
196 }
197
198 ParsedElement parsedElement =
199 FindMatchingElement(temp_string.substr(pos, next - pos + 1));
200
201 if (!parsedElement.Active) {
202 util::logf("Error parsing message: %s", temp_string);
203 break;
204 } else if (parsedElement.Parent.Token == DICTIONARYTOKEN) {
205 bytes.push_back(parsedElement.Value);
206 } else {
207 bytes.push_back(parsedElement.Parent.ID);
208
209 if (parsedElement.Parent.HasArgument) {
210 bytes.push_back(parsedElement.Value);
211 }
212 }
213
214 pos = next + 1;
215 continue;
216 } else {
217 uint8_t bb = FindMatchingCharacter(temp_string[pos++]);
218
219 if (bb != 0xFF) {
220 bytes.push_back(bb);
221 }
222 }
223 }
224
225 return bytes;
226}
227
229 MessageParseResult result;
230 std::string temp_string(str);
231 size_t pos = 0;
232 bool warned_newline = false;
233
234 while (pos < temp_string.size()) {
235 char current = temp_string[pos];
236 if (current == '\r' || current == '\n') {
237 if (!warned_newline) {
238 result.warnings.push_back(
239 "Literal newlines are ignored; use [1], [2], [3], [V], or [K] "
240 "tokens for line breaks.");
241 warned_newline = true;
242 }
243 pos++;
244 continue;
245 }
246
247 if (current == '[') {
248 size_t close = temp_string.find(']', pos);
249 if (close == std::string::npos) {
250 result.errors.push_back(
251 absl::StrFormat("Unclosed token starting at position %zu", pos));
252 break;
253 }
254
255 std::string token = temp_string.substr(pos, close - pos + 1);
256 ParsedElement parsed_element = FindMatchingElement(token);
257
258 if (!parsed_element.Active) {
259 result.errors.push_back(absl::StrFormat("Unknown token: %s", token));
260 pos = close + 1;
261 continue;
262 }
263
264 if (!parsed_element.Parent.HasArgument) {
265 if (token != parsed_element.Parent.GetParamToken()) {
266 result.errors.push_back(absl::StrFormat("Unknown token: %s", token));
267 pos = close + 1;
268 continue;
269 }
270 }
271
272 if (parsed_element.Parent.Token == DICTIONARYTOKEN) {
273 result.bytes.push_back(parsed_element.Value);
274 } else {
275 result.bytes.push_back(parsed_element.Parent.ID);
276 if (parsed_element.Parent.HasArgument) {
277 result.bytes.push_back(parsed_element.Value);
278 }
279 }
280
281 pos = close + 1;
282 continue;
283 }
284
285 uint8_t bb = FindMatchingCharacter(current);
286 if (bb == 0xFF) {
287 result.errors.push_back(absl::StrFormat(
288 "Unsupported character '%c' at position %zu", current, pos));
289 pos++;
290 continue;
291 }
292
293 result.bytes.push_back(bb);
294 pos++;
295 }
296
297 return result;
298}
299
301 switch (bank) {
303 return "vanilla";
305 return "expanded";
306 }
307 return "vanilla";
308}
309
310absl::StatusOr<MessageBank> MessageBankFromString(std::string_view value) {
311 const std::string lowered = absl::AsciiStrToLower(std::string(value));
312 if (lowered == "vanilla") {
314 }
315 if (lowered == "expanded") {
317 }
318 return absl::InvalidArgumentError(
319 absl::StrFormat("Unknown message bank: %s", std::string(value)));
320}
321
322std::vector<DictionaryEntry> BuildDictionaryEntries(Rom* rom) {
323 std::vector<DictionaryEntry> AllDictionaries;
324 for (int i = 0; i < kNumDictionaryEntries; i++) {
325 std::vector<uint8_t> bytes;
326 std::stringstream stringBuilder;
327
328 int address = SnesToPc(
329 kTextData + (rom->data()[kPointersDictionaries + (i * 2) + 1] << 8) +
330 rom->data()[kPointersDictionaries + (i * 2)]);
331
332 int temppush_backress =
334 (rom->data()[kPointersDictionaries + ((i + 1) * 2) + 1] << 8) +
335 rom->data()[kPointersDictionaries + ((i + 1) * 2)]);
336
337 while (address < temppush_backress) {
338 uint8_t uint8_tDictionary = rom->data()[address++];
339 bytes.push_back(uint8_tDictionary);
340 stringBuilder << ParseTextDataByte(uint8_tDictionary);
341 }
342
343 AllDictionaries.push_back(DictionaryEntry{(uint8_t)i, stringBuilder.str()});
344 }
345
346 std::ranges::sort(AllDictionaries,
347 [](const DictionaryEntry& a, const DictionaryEntry& b) {
348 return a.Contents.size() > b.Contents.size();
349 });
350
351 return AllDictionaries;
352}
353
355 std::string str, const std::vector<DictionaryEntry>& dictionary) {
356 std::string temp = std::move(str);
357 for (const auto& entry : dictionary) {
358 if (entry.ContainedInString(temp)) {
359 temp = entry.ReplaceInstancesOfIn(temp);
360 }
361 }
362 return temp;
363}
364
365std::optional<size_t> FindTextMatch(std::string_view text,
366 std::string_view query, size_t start_pos,
367 bool case_sensitive,
368 bool match_whole_word) {
369 if (query.empty() || start_pos > text.size()) {
370 return std::nullopt;
371 }
372
373 std::string haystack_storage;
374 std::string query_storage;
375 std::string_view haystack = text;
376 std::string_view needle = query;
377 if (!case_sensitive) {
378 haystack_storage = LowercaseCopy(text);
379 query_storage = LowercaseCopy(query);
380 haystack = haystack_storage;
381 needle = query_storage;
382 }
383
384 size_t pos = haystack.find(needle, start_pos);
385 while (pos != std::string::npos) {
386 if (!match_whole_word || MatchesWholeWordAt(text, pos, query.size())) {
387 return pos;
388 }
389 pos = haystack.find(needle, pos + 1);
390 }
391
392 return std::nullopt;
393}
394
395int ReplaceTextMatches(std::string* text, std::string_view query,
396 std::string_view replacement, size_t start_pos,
397 bool replace_all, bool case_sensitive,
398 bool match_whole_word, size_t* first_replaced_pos) {
399 if (!text || query.empty() || start_pos > text->size()) {
400 return 0;
401 }
402
403 int replacements = 0;
404 size_t cursor = start_pos;
405 while (true) {
406 const auto match_pos =
407 FindTextMatch(*text, query, cursor, case_sensitive, match_whole_word);
408 if (!match_pos.has_value()) {
409 break;
410 }
411
412 text->replace(*match_pos, query.size(), replacement);
413 if (replacements == 0 && first_replaced_pos != nullptr) {
414 *first_replaced_pos = *match_pos;
415 }
416 replacements++;
417
418 cursor = *match_pos + replacement.size();
419 if (!replace_all) {
420 break;
421 }
422
423 if (cursor > text->size()) {
424 break;
425 }
426 }
427
428 return replacements;
429}
430
432 uint8_t value, const std::vector<DictionaryEntry>& dictionary) {
433 for (const auto& entry : dictionary) {
434 if (entry.ID + DICTOFF == value) {
435 return entry;
436 }
437 }
438 return DictionaryEntry();
439}
440
441absl::StatusOr<MessageData> ParseSingleMessage(
442 const std::vector<uint8_t>& rom_data, int* current_pos) {
443 if (current_pos == nullptr) {
444 return absl::InvalidArgumentError("current_pos is null");
445 }
446 if (*current_pos < 0 ||
447 static_cast<size_t>(*current_pos) >= rom_data.size()) {
448 return absl::OutOfRangeError("current_pos is out of range");
449 }
450
451 MessageData message_data;
452 int pos = *current_pos;
453 uint8_t current_byte;
454 std::vector<uint8_t> temp_bytes_raw;
455 std::vector<uint8_t> temp_bytes_parsed;
456 std::string current_message_raw;
457 std::string current_message_parsed;
458
459 // Read the message data
460 while (pos < static_cast<int>(rom_data.size())) {
461 current_byte = rom_data[pos++];
462
463 if (current_byte == kMessageTerminator) {
464 message_data.ID = message_data.ID + 1;
465 message_data.Address = pos;
466 message_data.RawString = current_message_raw;
467 message_data.Data = temp_bytes_raw;
468 message_data.DataParsed = temp_bytes_parsed;
469 message_data.ContentsParsed = current_message_parsed;
470
471 temp_bytes_raw.clear();
472 temp_bytes_parsed.clear();
473 current_message_raw.clear();
474 current_message_parsed.clear();
475
476 *current_pos = pos;
477 return message_data;
478 } else if (current_byte == 0xFF) {
479 return absl::InvalidArgumentError("message terminator not found");
480 }
481
482 temp_bytes_raw.push_back(current_byte);
483
484 // Check for command.
485 auto text_element = FindMatchingCommand(current_byte);
486 if (text_element != std::nullopt) {
487 temp_bytes_parsed.push_back(current_byte);
488 if (text_element->HasArgument) {
489 if (pos >= static_cast<int>(rom_data.size())) {
490 return absl::OutOfRangeError("message command argument out of range");
491 }
492 uint8_t arg_byte = rom_data[pos++];
493 temp_bytes_raw.push_back(arg_byte);
494 temp_bytes_parsed.push_back(arg_byte);
495 current_message_raw.append(text_element->GetParamToken(arg_byte));
496 current_message_parsed.append(text_element->GetParamToken(arg_byte));
497 } else {
498 current_message_raw.append(text_element->GetParamToken());
499 current_message_parsed.append(text_element->GetParamToken());
500 }
501 continue;
502 }
503
504 // Check for special characters.
505 if (auto special_element = FindMatchingSpecial(current_byte);
506 special_element != std::nullopt) {
507 current_message_raw.append(special_element->GetParamToken());
508 current_message_parsed.append(special_element->GetParamToken());
509 temp_bytes_parsed.push_back(current_byte);
510 continue;
511 }
512
513 // Check for dictionary.
514 int8_t dictionary = FindDictionaryEntry(current_byte);
515 if (dictionary >= 0) {
516 std::string token = absl::StrFormat(
517 "[%s:%02X]", DICTIONARYTOKEN, static_cast<unsigned char>(dictionary));
518 current_message_raw.append(token);
519 current_message_parsed.append(token);
520 temp_bytes_parsed.push_back(current_byte);
521 continue;
522 }
523
524 // Everything else.
525 if (CharEncoder.contains(current_byte)) {
526 std::string str = "";
527 str.push_back(CharEncoder.at(current_byte));
528 current_message_raw.append(str);
529 current_message_parsed.append(str);
530 temp_bytes_parsed.push_back(current_byte);
531 }
532 }
533
534 *current_pos = pos;
535 return absl::InvalidArgumentError("message terminator not found");
536}
537
538std::vector<std::string> ParseMessageData(
539 std::vector<MessageData>& message_data,
540 const std::vector<DictionaryEntry>& dictionary_entries) {
541 std::vector<std::string> parsed_messages;
542
543 for (auto& message : message_data) {
544 std::string parsed_message = "";
545 // Use index-based loop to properly skip argument bytes
546 for (size_t pos = 0; pos < message.Data.size(); ++pos) {
547 uint8_t byte = message.Data[pos];
548
549 // Check for text commands first (they may have arguments to skip)
550 auto text_element = FindMatchingCommand(byte);
551 if (text_element != std::nullopt) {
552 // Add newline for certain commands
553 if (text_element->ID == kScrollVertical || text_element->ID == kLine2 ||
554 text_element->ID == kLine3) {
555 parsed_message.append("\n");
556 }
557 // If command has an argument, get it from next byte and skip it
558 if (text_element->HasArgument && pos + 1 < message.Data.size()) {
559 uint8_t arg_byte = message.Data[pos + 1];
560 parsed_message.append(text_element->GetParamToken(arg_byte));
561 pos++; // Skip the argument byte
562 } else {
563 parsed_message.append(text_element->GetParamToken());
564 }
565 continue; // Move to next byte
566 }
567
568 // Check for special characters
569 auto special_element = FindMatchingSpecial(byte);
570 if (special_element != std::nullopt) {
571 parsed_message.append(special_element->GetParamToken());
572 continue;
573 }
574
575 // Check for dictionary entries
576 if (byte >= DICTOFF && byte < (DICTOFF + 97)) {
577 DictionaryEntry dic_entry;
578 for (const auto& entry : dictionary_entries) {
579 if (entry.ID == byte - DICTOFF) {
580 dic_entry = entry;
581 break;
582 }
583 }
584 parsed_message.append(dic_entry.Contents);
585 continue;
586 }
587
588 // Finally check for regular characters
589 if (CharEncoder.contains(byte)) {
590 parsed_message.push_back(CharEncoder.at(byte));
591 }
592 }
593 parsed_messages.push_back(parsed_message);
594 }
595
596 return parsed_messages;
597}
598
599std::vector<MessageData> ReadAllTextData(uint8_t* rom, int pos, int max_pos,
600 bool allow_bank_switch) {
601 std::vector<MessageData> list_of_texts;
602 int message_id = 0;
603
604 if (!rom) {
605 return list_of_texts;
606 }
607 if (max_pos > 0 && (pos < 0 || pos >= max_pos)) {
608 return list_of_texts;
609 }
610
611 std::vector<uint8_t> raw_message;
612 std::vector<uint8_t> parsed_message;
613 std::string current_raw_message;
614 std::string current_parsed_message;
615
616 bool did_bank_switch = false;
617 uint8_t current_byte = 0;
618 while (current_byte != 0xFF) {
619 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
620 break;
621 current_byte = rom[pos++];
622 if (current_byte == kMessageTerminator) {
623 list_of_texts.push_back(
624 MessageData(message_id++, pos, current_raw_message, raw_message,
625 current_parsed_message, parsed_message));
626 raw_message.clear();
627 parsed_message.clear();
628 current_raw_message.clear();
629 current_parsed_message.clear();
630 continue;
631 } else if (current_byte == 0xFF) {
632 break;
633 }
634
635 raw_message.push_back(current_byte);
636
637 auto text_element = FindMatchingCommand(current_byte);
638 if (text_element != std::nullopt) {
639 parsed_message.push_back(current_byte);
640 if (text_element->HasArgument) {
641 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
642 break;
643 current_byte = rom[pos++];
644 raw_message.push_back(current_byte);
645 parsed_message.push_back(current_byte);
646 }
647
648 current_raw_message.append(text_element->GetParamToken(current_byte));
649 current_parsed_message.append(text_element->GetParamToken(current_byte));
650
651 if (allow_bank_switch && text_element->Token == kBankToken &&
652 !did_bank_switch) {
653 did_bank_switch = true;
654 pos = kTextData2;
655 }
656
657 continue;
658 }
659
660 // Check for special characters.
661 auto special_element = FindMatchingSpecial(current_byte);
662 if (special_element != std::nullopt) {
663 current_raw_message.append(special_element->GetParamToken());
664 current_parsed_message.append(special_element->GetParamToken());
665 parsed_message.push_back(current_byte);
666 continue;
667 }
668
669 // Check for dictionary.
670 int8_t dictionary = FindDictionaryEntry(current_byte);
671 if (dictionary >= 0) {
672 current_raw_message.append(
673 absl::StrFormat("[%s:%02X]", DICTIONARYTOKEN,
674 static_cast<unsigned char>(dictionary)));
675
676 // Safety: bounds-check dictionary pointer reads and dictionary expansion.
677 // This parser is used by tooling (RomDoctor) that may run on dummy or
678 // partially-initialized ROM buffers.
679 const int ptr_a = kPointersDictionaries + (dictionary * 2);
680 const int ptr_b = kPointersDictionaries + ((dictionary + 1) * 2);
681 if (max_pos > 0) {
682 if (ptr_a < 0 || ptr_a + 1 >= max_pos || ptr_b < 0 ||
683 ptr_b + 1 >= max_pos) {
684 continue;
685 }
686 }
687
688 uint32_t address =
689 Get24LocalFromPC(rom, kPointersDictionaries + (dictionary * 2));
690 uint32_t address_end =
691 Get24LocalFromPC(rom, kPointersDictionaries + ((dictionary + 1) * 2));
692
693 if (max_pos > 0) {
694 const uint32_t max_u = static_cast<uint32_t>(max_pos);
695 if (address >= max_u || address_end > max_u || address_end < address) {
696 continue;
697 }
698 }
699
700 for (uint32_t i = address; i < address_end; i++) {
701 if (max_pos > 0 && i >= static_cast<uint32_t>(max_pos))
702 break;
703 parsed_message.push_back(rom[i]);
704 current_parsed_message.append(ParseTextDataByte(rom[i]));
705 }
706
707 continue;
708 }
709
710 // Everything else.
711 if (CharEncoder.contains(current_byte)) {
712 std::string str = "";
713 str.push_back(CharEncoder.at(current_byte));
714 current_raw_message.append(str);
715 current_parsed_message.append(str);
716 parsed_message.push_back(current_byte);
717 }
718 }
719
720 return list_of_texts;
721}
722
723absl::Status LoadExpandedMessages(std::string& expanded_message_path,
724 std::vector<std::string>& parsed_messages,
725 std::vector<MessageData>& expanded_messages,
726 std::vector<DictionaryEntry>& dictionary) {
727 static Rom expanded_message_rom;
728 if (!expanded_message_rom.LoadFromFile(expanded_message_path).ok()) {
729 return absl::InternalError("Failed to load expanded message ROM");
730 }
731 expanded_messages = ReadAllTextData(expanded_message_rom.mutable_data(), 0);
732 auto parsed_expanded_messages =
733 ParseMessageData(expanded_messages, dictionary);
734 // Insert into parsed_messages
735 for (const auto& expanded_message : expanded_messages) {
736 parsed_messages.push_back(parsed_expanded_messages[expanded_message.ID]);
737 }
738 return absl::OkStatus();
739}
740
742 const std::vector<MessageData>& messages) {
743 nlohmann::json j = nlohmann::json::array();
744 for (const auto& msg : messages) {
745 j.push_back({{"id", msg.ID},
746 {"address", msg.Address},
747 {"raw_string", msg.RawString},
748 {"parsed_string", msg.ContentsParsed}});
749 }
750 return j;
751}
752
753absl::Status ExportMessagesToJson(const std::string& path,
754 const std::vector<MessageData>& messages) {
755 try {
756 nlohmann::json j = SerializeMessagesToJson(messages);
757 std::ofstream file(path);
758 if (!file.is_open()) {
759 return absl::InternalError(
760 absl::StrFormat("Failed to open file for writing: %s", path));
761 }
762 file << j.dump(2); // Pretty print with 2-space indent
763 return absl::OkStatus();
764 } catch (const std::exception& e) {
765 return absl::InternalError(
766 absl::StrFormat("JSON export failed: %s", e.what()));
767 }
768}
769
771 const std::vector<MessageData>& vanilla,
772 const std::vector<MessageData>& expanded) {
773 nlohmann::json j;
774 j["format"] = "yaze-message-bundle";
775 j["version"] = kMessageBundleVersion;
776 j["counts"] = {{"vanilla", vanilla.size()}, {"expanded", expanded.size()}};
777 j["messages"] = nlohmann::json::array();
778
779 auto append_messages = [&j](const std::vector<MessageData>& messages,
780 MessageBank bank) {
781 for (const auto& msg : messages) {
782 nlohmann::json entry;
783 entry["id"] = msg.ID;
784 entry["bank"] = MessageBankToString(bank);
785 entry["address"] = msg.Address;
786 entry["raw"] = msg.RawString;
787 entry["parsed"] = msg.ContentsParsed;
788 entry["text"] =
789 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
790 entry["length"] = msg.Data.size();
791 const std::string validation_text =
792 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
793 auto warnings = ValidateMessageLineWidths(validation_text);
794 if (!warnings.empty()) {
795 entry["line_width_warnings"] = warnings;
796 }
797 j["messages"].push_back(entry);
798 }
799 };
800
801 append_messages(vanilla, MessageBank::kVanilla);
802 append_messages(expanded, MessageBank::kExpanded);
803
804 return j;
805}
806
808 const std::string& path, const std::vector<MessageData>& vanilla,
809 const std::vector<MessageData>& expanded) {
810 try {
811 nlohmann::json j = SerializeMessageBundle(vanilla, expanded);
812 std::ofstream file(path);
813 if (!file.is_open()) {
814 return absl::InternalError(
815 absl::StrFormat("Failed to open file for writing: %s", path));
816 }
817 file << j.dump(2);
818 return absl::OkStatus();
819 } catch (const std::exception& e) {
820 return absl::InternalError(
821 absl::StrFormat("Message bundle export failed: %s", e.what()));
822 }
823}
824
825namespace {
826absl::StatusOr<MessageBundleEntry> ParseMessageBundleEntry(
827 const nlohmann::json& entry, MessageBank default_bank) {
828 if (!entry.is_object()) {
829 return absl::InvalidArgumentError("Message entry must be an object");
830 }
831
832 MessageBundleEntry result;
833 result.id = entry.value("id", -1);
834 if (result.id < 0) {
835 return absl::InvalidArgumentError("Message entry missing valid id");
836 }
837
838 if (entry.contains("bank")) {
839 if (!entry["bank"].is_string()) {
840 return absl::InvalidArgumentError("Message entry bank must be string");
841 }
842 auto bank_or = MessageBankFromString(entry["bank"].get<std::string>());
843 if (!bank_or.ok()) {
844 return bank_or.status();
845 }
846 result.bank = bank_or.value();
847 } else {
848 result.bank = default_bank;
849 }
850
851 if (entry.contains("raw") && entry["raw"].is_string()) {
852 result.raw = entry["raw"].get<std::string>();
853 } else if (entry.contains("raw_string") && entry["raw_string"].is_string()) {
854 result.raw = entry["raw_string"].get<std::string>();
855 }
856
857 if (entry.contains("parsed") && entry["parsed"].is_string()) {
858 result.parsed = entry["parsed"].get<std::string>();
859 } else if (entry.contains("parsed_string") &&
860 entry["parsed_string"].is_string()) {
861 result.parsed = entry["parsed_string"].get<std::string>();
862 }
863
864 if (entry.contains("text") && entry["text"].is_string()) {
865 result.text = entry["text"].get<std::string>();
866 }
867
868 if (result.text.empty()) {
869 if (!result.raw.empty()) {
870 result.text = result.raw;
871 } else if (!result.parsed.empty()) {
872 result.text = result.parsed;
873 }
874 }
875
876 if (result.text.empty()) {
877 return absl::InvalidArgumentError(
878 absl::StrFormat("Message entry %d missing text content", result.id));
879 }
880
881 return result;
882}
883} // namespace
884
885absl::StatusOr<std::vector<MessageBundleEntry>> ParseMessageBundleJson(
886 const nlohmann::json& json) {
887 std::vector<MessageBundleEntry> entries;
888
889 if (json.is_array()) {
890 for (const auto& entry : json) {
891 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
892 if (!parsed_or.ok()) {
893 return parsed_or.status();
894 }
895 entries.push_back(parsed_or.value());
896 }
897 return entries;
898 }
899
900 if (!json.is_object()) {
901 return absl::InvalidArgumentError("Message bundle JSON must be object");
902 }
903
904 if (json.contains("version") && json["version"].is_number_integer()) {
905 int version = json["version"].get<int>();
906 if (version != kMessageBundleVersion) {
907 return absl::InvalidArgumentError(
908 absl::StrFormat("Unsupported message bundle version: %d", version));
909 }
910 }
911
912 if (!json.contains("messages") || !json["messages"].is_array()) {
913 return absl::InvalidArgumentError("Message bundle missing messages array");
914 }
915
916 for (const auto& entry : json["messages"]) {
917 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
918 if (!parsed_or.ok()) {
919 return parsed_or.status();
920 }
921 entries.push_back(parsed_or.value());
922 }
923
924 return entries;
925}
926
927absl::StatusOr<std::vector<MessageBundleEntry>> LoadMessageBundleFromJson(
928 const std::string& path) {
929 std::ifstream file(path);
930 if (!file.is_open()) {
931 return absl::NotFoundError(
932 absl::StrFormat("Cannot open message bundle: %s", path));
933 }
934
935 nlohmann::json json;
936 try {
937 file >> json;
938 } catch (const std::exception& e) {
939 return absl::InvalidArgumentError(
940 absl::StrFormat("Failed to parse JSON: %s", e.what()));
941 }
942
943 return ParseMessageBundleJson(json);
944}
945
946// ===========================================================================
947// Line Width Validation
948// ===========================================================================
949
950std::vector<std::string> ValidateMessageLineWidths(const std::string& message) {
951 std::vector<std::string> warnings;
952
953 // Split message into lines on line-break tokens: [1], [2], [3], [V], [K]
954 // We walk through the string, counting visible characters per line.
955 int line_num = 1;
956 int visible_chars = 0;
957 bool all_spaces_this_line = true;
958 size_t pos = 0;
959
960 while (pos < message.size()) {
961 if (message[pos] == '[') {
962 // Find the closing bracket
963 size_t close = message.find(']', pos);
964 if (close == std::string::npos)
965 break;
966
967 std::string token = message.substr(pos, close - pos + 1);
968 pos = close + 1;
969
970 // Check if this token is a line-breaking command
971 // Line breaks: [1], [2], [3], [V], [K]
972 if (token == "[1]" || token == "[2]" || token == "[3]" ||
973 token == "[V]" || token == "[K]") {
974 // Check current line width before breaking.
975 // Exempt whitespace-only lines (used as screen clears in ALTTP).
976 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
977 warnings.push_back(
978 absl::StrFormat("Line %d: %d visible characters (max %d)",
979 line_num, visible_chars, kMaxLineWidth));
980 }
981 line_num++;
982 visible_chars = 0;
983 all_spaces_this_line = true;
984 }
985 // Other command tokens ([W:02], [S:03], [SFX:2D], [L], [...], etc.)
986 // are not counted as visible characters - they're control codes or
987 // expand to game-rendered content that we can't measure in chars.
988 // Exception: [L] expands to player name but width varies (1-6 chars).
989 // For simplicity, we don't count command tokens.
990 continue;
991 }
992
993 // Regular visible character
994 if (message[pos] != ' ')
995 all_spaces_this_line = false;
996 visible_chars++;
997 pos++;
998 }
999
1000 // Check the last line (exempt whitespace-only lines)
1001 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
1002 warnings.push_back(
1003 absl::StrFormat("Line %d: %d visible characters (max %d)", line_num,
1004 visible_chars, kMaxLineWidth));
1005 }
1006
1007 return warnings;
1008}
1009
1010// ===========================================================================
1011// Org Format (.org) Import/Export
1012// ===========================================================================
1013
1014std::optional<std::pair<int, std::string>> ParseOrgHeader(
1015 const std::string& line) {
1016 // Expected format: "** XX - Label Text"
1017 // where XX is a hex message ID
1018 if (line.size() < 6 || line[0] != '*' || line[1] != '*' || line[2] != ' ') {
1019 return std::nullopt;
1020 }
1021
1022 // Find the " - " separator
1023 size_t sep = line.find(" - ", 3);
1024 if (sep == std::string::npos) {
1025 return std::nullopt;
1026 }
1027
1028 // Parse hex ID between "** " and " - "
1029 std::string hex_id = line.substr(3, sep - 3);
1030 int message_id;
1031 try {
1032 message_id = std::stoi(hex_id, nullptr, 16);
1033 } catch (const std::exception&) {
1034 return std::nullopt;
1035 }
1036
1037 // Extract label after " - "
1038 std::string label = line.substr(sep + 3);
1039
1040 return std::make_pair(message_id, label);
1041}
1042
1043std::vector<std::pair<int, std::string>> ParseOrgContent(
1044 const std::string& content) {
1045 std::vector<std::pair<int, std::string>> messages;
1046 std::istringstream stream(content);
1047 std::string line;
1048
1049 int current_id = -1;
1050 std::string current_body;
1051
1052 while (std::getline(stream, line)) {
1053 // Check if this is a header line
1054 auto header = ParseOrgHeader(line);
1055 if (header.has_value()) {
1056 // Save previous message if any
1057 if (current_id >= 0) {
1058 // Trim trailing newline from body
1059 while (!current_body.empty() && current_body.back() == '\n') {
1060 current_body.pop_back();
1061 }
1062 messages.push_back({current_id, current_body});
1063 }
1064
1065 current_id = header->first;
1066 current_body.clear();
1067 continue;
1068 }
1069
1070 // Skip top-level org headers (single *)
1071 if (!line.empty() && line[0] == '*' &&
1072 (line.size() < 2 || line[1] != '*')) {
1073 continue;
1074 }
1075
1076 // Accumulate body text
1077 if (current_id >= 0) {
1078 if (!current_body.empty()) {
1079 current_body += "\n";
1080 }
1081 current_body += line;
1082 }
1083 }
1084
1085 // Save last message
1086 if (current_id >= 0) {
1087 while (!current_body.empty() && current_body.back() == '\n') {
1088 current_body.pop_back();
1089 }
1090 messages.push_back({current_id, current_body});
1091 }
1092
1093 return messages;
1094}
1095
1097 const std::vector<std::pair<int, std::string>>& messages,
1098 const std::vector<std::string>& labels) {
1099 std::string output;
1100 output += "* Oracle of Secrets English Dialogue\n";
1101
1102 for (size_t i = 0; i < messages.size(); ++i) {
1103 const auto& [msg_id, body] = messages[i];
1104 std::string label = (i < labels.size())
1105 ? labels[i]
1106 : absl::StrFormat("Message %02X", msg_id);
1107
1108 output += absl::StrFormat("** %02X - %s\n", msg_id, label);
1109 output += body;
1110 output += "\n\n";
1111 }
1112
1113 return output;
1114}
1115
1116// ===========================================================================
1117// Expanded Message Bank
1118// ===========================================================================
1119
1120std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos) {
1121 // Expanded messages occupy one contiguous region. A vanilla [BANK] command
1122 // must not redirect parsing into the vanilla second text bank.
1123 return ReadAllTextData(rom, pos, /*max_pos=*/-1,
1124 /*allow_bank_switch=*/false);
1125}
1126
1127std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos, int end) {
1128 if (end < pos) {
1129 return {};
1130 }
1131 // ReadAllTextData's max_pos is exclusive; expanded-region ends are
1132 // configured as inclusive addresses.
1133 return ReadAllTextData(rom, pos, end + 1, /*allow_bank_switch=*/false);
1134}
1135
1136absl::Status WriteExpandedTextData(Rom* rom, int start, int end,
1137 const std::vector<std::string>& messages) {
1138 if (rom == nullptr || !rom->is_loaded()) {
1139 return absl::InvalidArgumentError("ROM not loaded");
1140 }
1141 if (start < 0 || end < start) {
1142 return absl::InvalidArgumentError("Invalid expanded message region");
1143 }
1144
1145 const int capacity = end - start + 1;
1146 if (capacity <= 0) {
1147 return absl::InvalidArgumentError(
1148 "Expanded message region has no capacity");
1149 }
1150
1151 const auto& data = rom->vector();
1152 if (end >= static_cast<int>(data.size())) {
1153 return absl::OutOfRangeError("Expanded message region out of ROM range");
1154 }
1155
1156 // Serialize into a contiguous buffer, then do a single ROM write for safety
1157 // and determinism (and to honor write fences).
1158 std::vector<uint8_t> blob;
1159 blob.reserve(static_cast<size_t>(capacity));
1160
1161 int used = 0;
1162 for (size_t i = 0; i < messages.size(); ++i) {
1163 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1164 if (!parsed.ok()) {
1165 return absl::InvalidArgumentError(
1166 absl::StrFormat("Expanded message %d is invalid: %s",
1167 static_cast<int>(i), parsed.errors.front()));
1168 }
1169 if (messages[i].find("[BANK]") != std::string::npos) {
1170 return absl::InvalidArgumentError(absl::StrFormat(
1171 "Expanded message %d contains [BANK], which is only valid in the "
1172 "vanilla message stream",
1173 static_cast<int>(i)));
1174 }
1175 auto bytes = std::move(parsed.bytes);
1176 const int needed = static_cast<int>(bytes.size()) + 1; // +0x7F
1177
1178 // Always reserve space for the final 0xFF.
1179 if (used + needed + 1 > capacity) {
1180 return absl::ResourceExhaustedError(absl::StrFormat(
1181 "Expanded message data exceeds bank boundary "
1182 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
1183 static_cast<int>(i), used, needed, capacity, end));
1184 }
1185
1186 blob.insert(blob.end(), bytes.begin(), bytes.end());
1187 blob.push_back(kMessageTerminator);
1188 used += needed;
1189 }
1190
1191 if (used + 1 > capacity) {
1192 return absl::ResourceExhaustedError(
1193 "No space for end-of-region marker (0xFF)");
1194 }
1195 blob.push_back(0xFF);
1196
1197 // ROM safety: this writer must only touch the expanded message region.
1198 // NOTE: `end` is inclusive; convert to half-open for the fence.
1200 const uint32_t fence_start = static_cast<uint32_t>(start);
1201 const uint32_t fence_end =
1202 static_cast<uint32_t>(static_cast<uint64_t>(end) + 1ULL);
1203 RETURN_IF_ERROR(fence.Allow(fence_start, fence_end, "ExpandedMessageBank"));
1204 yaze::rom::ScopedWriteFence scope(rom, &fence);
1205
1206 return rom->WriteVector(start, std::move(blob));
1207}
1208
1209absl::Status WriteExpandedTextData(uint8_t* rom, int start, int end,
1210 const std::vector<std::string>& messages) {
1211 if (rom == nullptr || start < 0 || end < start) {
1212 return absl::InvalidArgumentError("Invalid expanded message region");
1213 }
1214
1215 const int capacity = end - start + 1;
1216 int used = 0;
1217 std::vector<std::vector<uint8_t>> encoded_messages;
1218 encoded_messages.reserve(messages.size());
1219
1220 for (size_t i = 0; i < messages.size(); ++i) {
1221 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1222 if (!parsed.ok()) {
1223 return absl::InvalidArgumentError(
1224 absl::StrFormat("Expanded message %d is invalid: %s",
1225 static_cast<int>(i), parsed.errors.front()));
1226 }
1227 if (messages[i].find("[BANK]") != std::string::npos) {
1228 return absl::InvalidArgumentError(absl::StrFormat(
1229 "Expanded message %d contains [BANK], which is only valid in the "
1230 "vanilla message stream",
1231 static_cast<int>(i)));
1232 }
1233 auto bytes = std::move(parsed.bytes);
1234
1235 const int needed = static_cast<int>(bytes.size()) + 1; // +1 for 0x7F
1236 if (used + needed + 1 > capacity) {
1237 return absl::ResourceExhaustedError(
1238 absl::StrFormat("Expanded message data exceeds bank boundary "
1239 "(at message %d, used %d, end 0x%06X)",
1240 static_cast<int>(i), used, end));
1241 }
1242 used += needed;
1243 encoded_messages.push_back(std::move(bytes));
1244 }
1245
1246 int pos = start;
1247 for (const auto& bytes : encoded_messages) {
1248 for (uint8_t byte : bytes) {
1249 rom[pos++] = byte;
1250 }
1251 rom[pos++] = kMessageTerminator;
1252 }
1253
1254 rom[pos++] = 0xFF;
1255
1256 return absl::OkStatus();
1257}
1258
1259std::vector<std::pair<uint32_t, uint32_t>>
1261 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1262 ranges.reserve(writes_.size());
1263 for (const VanillaMessageWrite& write : writes_) {
1264 ranges.emplace_back(write.start(), write.end());
1265 }
1266 return ranges;
1267}
1268
1269absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
1270 const std::vector<MessageData>& messages,
1271 std::optional<size_t> expected_message_count) {
1272 if (expected_message_count.has_value() &&
1273 messages.size() != *expected_message_count) {
1274 return absl::FailedPreconditionError(
1275 absl::StrFormat("Vanilla message count mismatch: expected %zu, got %zu",
1276 *expected_message_count, messages.size()));
1277 }
1278
1279 constexpr size_t kPrimaryCapacity = kTextDataEnd - kTextData + 1;
1280 constexpr size_t kSecondaryCapacity = kTextData2End - kTextData2 + 1;
1281 std::vector<uint8_t> primary;
1282 std::vector<uint8_t> secondary;
1283 primary.reserve(kPrimaryCapacity);
1284 secondary.reserve(kSecondaryCapacity);
1285
1286 bool in_second_bank = false;
1287 size_t bank_switch_count = 0;
1288 auto append_byte = [&](uint8_t value, size_t message_index) -> absl::Status {
1289 auto& destination = in_second_bank ? secondary : primary;
1290 const size_t capacity =
1291 in_second_bank ? kSecondaryCapacity : kPrimaryCapacity;
1292 if (destination.size() >= capacity) {
1293 const uint32_t address =
1294 static_cast<uint32_t>(in_second_bank ? kTextData2 : kTextData) +
1295 static_cast<uint32_t>(destination.size());
1296 return absl::ResourceExhaustedError(absl::StrFormat(
1297 "Vanilla message %zu exceeds %s bank capacity at 0x%06X",
1298 message_index, in_second_bank ? "second" : "first", address));
1299 }
1300 destination.push_back(value);
1301 return absl::OkStatus();
1302 };
1303
1304 for (size_t message_index = 0; message_index < messages.size();
1305 ++message_index) {
1306 bool next_byte_is_command_argument = false;
1307 for (uint8_t value : messages[message_index].Data) {
1308 const bool is_command_argument = next_byte_is_command_argument;
1309 next_byte_is_command_argument = false;
1310 if (!is_command_argument &&
1311 (value == kMessageTerminator || value == 0xFF)) {
1312 return absl::InvalidArgumentError(absl::StrFormat(
1313 "Vanilla message %zu contains reserved stream marker 0x%02X",
1314 message_index, value));
1315 }
1316 const bool is_bank_switch =
1317 !is_command_argument && value == kBankSwitchCommand;
1318 if (is_bank_switch && bank_switch_count != 0) {
1319 return absl::InvalidArgumentError(absl::StrFormat(
1320 "Vanilla message %zu contains a duplicate [BANK] command",
1321 message_index));
1322 }
1323
1324 RETURN_IF_ERROR(append_byte(value, message_index));
1325 if (is_bank_switch) {
1326 ++bank_switch_count;
1327 in_second_bank = true;
1328 } else if (!is_command_argument) {
1329 const auto command = FindMatchingCommand(value);
1330 next_byte_is_command_argument =
1331 command.has_value() && command->HasArgument;
1332 }
1333 }
1334 if (next_byte_is_command_argument) {
1335 return absl::InvalidArgumentError(absl::StrFormat(
1336 "Vanilla message %zu ends with a command missing its argument",
1337 message_index));
1338 }
1339 RETURN_IF_ERROR(append_byte(kMessageTerminator, message_index));
1340 }
1341
1342 if (bank_switch_count != 1) {
1343 return absl::InvalidArgumentError(
1344 "Vanilla message stream is missing the required [BANK] command");
1345 }
1346 RETURN_IF_ERROR(append_byte(0xFF, messages.size()));
1347
1348 std::vector<VanillaMessageWrite> writes;
1349 writes.reserve(in_second_bank ? 2 : 1);
1350 writes.emplace_back(static_cast<uint32_t>(kTextData), std::move(primary));
1351 if (in_second_bank) {
1352 writes.emplace_back(static_cast<uint32_t>(kTextData2),
1353 std::move(secondary));
1354 }
1355 return VanillaMessageSavePlan(std::move(writes), messages.size(),
1356 bank_switch_count);
1357}
1358
1360 const VanillaMessageSavePlan& plan) {
1361 if (rom == nullptr || !rom->is_loaded()) {
1362 return absl::InvalidArgumentError("ROM not loaded");
1363 }
1364
1366 for (const VanillaMessageWrite& write : plan.writes()) {
1367 if (write.end() > rom->size()) {
1368 return absl::OutOfRangeError(absl::StrFormat(
1369 "Vanilla message write range [0x%06X, 0x%06X) is outside the ROM",
1370 write.start(), write.end()));
1371 }
1373 fence.Allow(write.start(), write.end(), "VanillaMessageBank"));
1374 }
1375
1376 ScopedRomTransaction transaction(*rom);
1377 yaze::rom::ScopedWriteFence fence_scope(rom, &fence);
1378 for (const VanillaMessageWrite& write : plan.writes()) {
1380 rom->WriteVector(static_cast<int>(write.start()), write.bytes()));
1381 }
1382 transaction.Commit();
1383 return absl::OkStatus();
1384}
1385
1386absl::Status WriteAllTextData(Rom* rom,
1387 const std::vector<MessageData>& messages) {
1389 const VanillaMessageSavePlan plan,
1390 BuildVanillaMessageSavePlan(messages, /*expected_message_count=*/{}));
1391 return ApplyVanillaMessageSavePlan(rom, plan);
1392}
1393
1394} // namespace editor
1395} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:270
auto mutable_data()
Definition rom.h:170
const auto & vector() const
Definition rom.h:173
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:703
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
static RomSettings & Get()
uint32_t GetAddressOr(const std::string &key, uint32_t default_value) const
const std::vector< VanillaMessageWrite > & writes() const
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
std::vector< VanillaMessageWrite > writes_
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr char kExpandedMessageEnd[]
constexpr char kExpandedMessageStart[]
bool MatchesWholeWordAt(std::string_view text, size_t pos, size_t len)
absl::StatusOr< MessageBundleEntry > ParseMessageBundleEntry(const nlohmann::json &entry, MessageBank default_bank)
uint8_t FindMatchingCharacter(char value)
const std::string kBankToken
nlohmann::json SerializeMessagesToJson(const std::vector< MessageData > &messages)
absl::StatusOr< MessageBank > MessageBankFromString(std::string_view value)
DictionaryEntry FindRealDictionaryEntry(uint8_t value, const std::vector< DictionaryEntry > &dictionary)
constexpr int kMaxLineWidth
int GetExpandedTextDataStart()
constexpr int kMessageBundleVersion
const std::string DICTIONARYTOKEN
constexpr uint8_t kScrollVertical
std::string ParseTextDataByte(uint8_t value)
absl::Status WriteAllTextData(Rom *rom, const std::vector< MessageData > &messages)
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
std::optional< std::pair< int, std::string > > ParseOrgHeader(const std::string &line)
std::string MessageBankToString(MessageBank bank)
constexpr int kExpandedTextDataEndDefault
constexpr int kTextData2
std::string ReplaceAllDictionaryWords(std::string str, const std::vector< DictionaryEntry > &dictionary)
absl::Status WriteExpandedTextData(Rom *rom, int start, int end, const std::vector< std::string > &messages)
nlohmann::json SerializeMessageBundle(const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr uint8_t kLine2
constexpr int kPointersDictionaries
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
constexpr int kNumDictionaryEntries
absl::StatusOr< MessageData > ParseSingleMessage(const std::vector< uint8_t > &rom_data, int *current_pos)
absl::StatusOr< std::vector< MessageBundleEntry > > ParseMessageBundleJson(const nlohmann::json &json)
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)
std::optional< TextElement > FindMatchingSpecial(uint8_t value)
constexpr uint8_t kMessageTerminator
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t kBankSwitchCommand
int ReplaceTextMatches(std::string *text, std::string_view query, std::string_view replacement, size_t start_pos, bool replace_all, bool case_sensitive, bool match_whole_word, size_t *first_replaced_pos)
std::optional< size_t > FindTextMatch(std::string_view text, std::string_view query, size_t start_pos, bool case_sensitive, bool match_whole_word)
std::vector< uint8_t > ParseMessageToData(std::string str)
absl::Status ExportMessagesToJson(const std::string &path, const std::vector< MessageData > &messages)
absl::Status ExportMessageBundleToJson(const std::string &path, const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr uint8_t DICTOFF
std::string ExportToOrgFormat(const std::vector< std::pair< int, std::string > > &messages, const std::vector< std::string > &labels)
absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
std::optional< TextElement > FindMatchingCommand(uint8_t b)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
ParsedElement FindMatchingElement(const std::string &str)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
std::vector< std::pair< int, std::string > > ParseOrgContent(const std::string &content)
constexpr int kExpandedTextDataDefault
constexpr uint8_t kLine3
int8_t FindDictionaryEntry(uint8_t value)
absl::Status ApplyVanillaMessageSavePlan(Rom *rom, const VanillaMessageSavePlan &plan)
constexpr int kTextDataEnd
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
uint32_t Get24LocalFromPC(uint8_t *data, int addr, bool pc=true)
Definition snes.h:30
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
std::vector< uint8_t > Data
std::vector< uint8_t > DataParsed
std::vector< uint8_t > bytes
std::vector< std::string > errors
std::vector< std::string > warnings
std::string GetParamToken(uint8_t value=0) const