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
354std::vector<std::vector<uint8_t>> ReadDictionaryEntryBytes(const uint8_t* rom,
355 size_t rom_size) {
356 // Pointers are offsets in bank $0E; the table holds one more pointer than
357 // entries, which ends the last entry.
358 const size_t table_end =
360 if (rom == nullptr || table_end > rom_size) {
361 return {};
362 }
363 auto entry_pc = [&](int index) -> uint32_t {
364 const size_t at = kPointersDictionaries + index * 2;
365 const uint16_t pointer = rom[at] | (rom[at + 1] << 8);
366 return SnesToPc(0x0E0000 | pointer);
367 };
368 std::vector<std::vector<uint8_t>> entries;
369 entries.reserve(kNumDictionaryEntries);
370 for (int index = 0; index < kNumDictionaryEntries; ++index) {
371 const uint32_t start = entry_pc(index);
372 const uint32_t end = entry_pc(index + 1);
373 if (end < start || end > rom_size) {
374 return {};
375 }
376 entries.emplace_back(rom + start, rom + end);
377 }
378 return entries;
379}
380
381std::vector<uint8_t> ExpandMessageDictionary(
382 const std::vector<uint8_t>& data,
383 const std::vector<std::vector<uint8_t>>& dictionary) {
384 std::vector<uint8_t> out;
385 out.reserve(data.size() * 2);
386 for (size_t index = 0; index < data.size(); ++index) {
387 const uint8_t value = data[index];
388 const size_t entry = value >= DICTOFF ? static_cast<size_t>(value - DICTOFF)
389 : dictionary.size();
390 if (entry < dictionary.size()) {
391 out.insert(out.end(), dictionary[entry].begin(), dictionary[entry].end());
392 continue;
393 }
394 out.push_back(value);
395 const auto command = FindMatchingCommand(value);
396 if (command.has_value() && command->HasArgument &&
397 index + 1 < data.size()) {
398 out.push_back(data[++index]);
399 }
400 }
401 return out;
402}
403
404std::vector<uint8_t> CompressMessageWithDictionary(
405 const std::vector<uint8_t>& data,
406 const std::vector<std::vector<uint8_t>>& dictionary) {
407 constexpr uint8_t kFirstCommandByte = 0x67;
408 std::vector<uint8_t> out;
409 out.reserve(data.size());
410
411 // Fewest bytes for one character run: cost[i] covers run[i..], and
412 // choice[i] is the dictionary entry used at i (-1 keeps the character).
413 auto compress_run = [&](size_t begin, size_t end) {
414 const size_t length = end - begin;
415 std::vector<size_t> cost(length + 1, 0);
416 std::vector<int> choice(length + 1, -1);
417 for (size_t i = length; i-- > 0;) {
418 cost[i] = cost[i + 1] + 1;
419 for (size_t entry = 0; entry < dictionary.size(); ++entry) {
420 const std::vector<uint8_t>& word = dictionary[entry];
421 if (word.size() < 2 || word.size() > length - i ||
422 !std::equal(
423 word.begin(), word.end(),
424 data.begin() + static_cast<std::ptrdiff_t>(begin + i))) {
425 continue;
426 }
427 if (cost[i + word.size()] + 1 < cost[i]) {
428 cost[i] = cost[i + word.size()] + 1;
429 choice[i] = static_cast<int>(entry);
430 }
431 }
432 }
433 for (size_t i = 0; i < length;) {
434 if (choice[i] < 0) {
435 out.push_back(data[begin + i]);
436 ++i;
437 } else {
438 out.push_back(static_cast<uint8_t>(DICTOFF + choice[i]));
439 i += dictionary[choice[i]].size();
440 }
441 }
442 };
443
444 size_t index = 0;
445 while (index < data.size()) {
446 if (data[index] < kFirstCommandByte) {
447 size_t run_end = index;
448 while (run_end < data.size() && data[run_end] < kFirstCommandByte) {
449 ++run_end;
450 }
451 compress_run(index, run_end);
452 index = run_end;
453 continue;
454 }
455 out.push_back(data[index]);
456 const auto command = FindMatchingCommand(data[index]);
457 if (command.has_value() && command->HasArgument &&
458 index + 1 < data.size()) {
459 out.push_back(data[index + 1]);
460 index += 2;
461 continue;
462 }
463 ++index;
464 }
465 return out;
466}
467
469 std::string str, const std::vector<DictionaryEntry>& dictionary) {
470 std::string temp = std::move(str);
471 for (const auto& entry : dictionary) {
472 if (entry.ContainedInString(temp)) {
473 temp = entry.ReplaceInstancesOfIn(temp);
474 }
475 }
476 return temp;
477}
478
479std::optional<size_t> FindTextMatch(std::string_view text,
480 std::string_view query, size_t start_pos,
481 bool case_sensitive,
482 bool match_whole_word) {
483 if (query.empty() || start_pos > text.size()) {
484 return std::nullopt;
485 }
486
487 std::string haystack_storage;
488 std::string query_storage;
489 std::string_view haystack = text;
490 std::string_view needle = query;
491 if (!case_sensitive) {
492 haystack_storage = LowercaseCopy(text);
493 query_storage = LowercaseCopy(query);
494 haystack = haystack_storage;
495 needle = query_storage;
496 }
497
498 size_t pos = haystack.find(needle, start_pos);
499 while (pos != std::string::npos) {
500 if (!match_whole_word || MatchesWholeWordAt(text, pos, query.size())) {
501 return pos;
502 }
503 pos = haystack.find(needle, pos + 1);
504 }
505
506 return std::nullopt;
507}
508
509int ReplaceTextMatches(std::string* text, std::string_view query,
510 std::string_view replacement, size_t start_pos,
511 bool replace_all, bool case_sensitive,
512 bool match_whole_word, size_t* first_replaced_pos) {
513 if (!text || query.empty() || start_pos > text->size()) {
514 return 0;
515 }
516
517 int replacements = 0;
518 size_t cursor = start_pos;
519 while (true) {
520 const auto match_pos =
521 FindTextMatch(*text, query, cursor, case_sensitive, match_whole_word);
522 if (!match_pos.has_value()) {
523 break;
524 }
525
526 text->replace(*match_pos, query.size(), replacement);
527 if (replacements == 0 && first_replaced_pos != nullptr) {
528 *first_replaced_pos = *match_pos;
529 }
530 replacements++;
531
532 cursor = *match_pos + replacement.size();
533 if (!replace_all) {
534 break;
535 }
536
537 if (cursor > text->size()) {
538 break;
539 }
540 }
541
542 return replacements;
543}
544
546 uint8_t value, const std::vector<DictionaryEntry>& dictionary) {
547 for (const auto& entry : dictionary) {
548 if (entry.ID + DICTOFF == value) {
549 return entry;
550 }
551 }
552 return DictionaryEntry();
553}
554
555absl::StatusOr<MessageData> ParseSingleMessage(
556 const std::vector<uint8_t>& rom_data, int* current_pos) {
557 if (current_pos == nullptr) {
558 return absl::InvalidArgumentError("current_pos is null");
559 }
560 if (*current_pos < 0 ||
561 static_cast<size_t>(*current_pos) >= rom_data.size()) {
562 return absl::OutOfRangeError("current_pos is out of range");
563 }
564
565 MessageData message_data;
566 int pos = *current_pos;
567 uint8_t current_byte;
568 std::vector<uint8_t> temp_bytes_raw;
569 std::vector<uint8_t> temp_bytes_parsed;
570 std::string current_message_raw;
571 std::string current_message_parsed;
572
573 // Read the message data
574 while (pos < static_cast<int>(rom_data.size())) {
575 current_byte = rom_data[pos++];
576
577 if (current_byte == kMessageTerminator) {
578 message_data.ID = message_data.ID + 1;
579 message_data.Address = pos;
580 message_data.RawString = current_message_raw;
581 message_data.Data = temp_bytes_raw;
582 message_data.DataParsed = temp_bytes_parsed;
583 message_data.ContentsParsed = current_message_parsed;
584
585 temp_bytes_raw.clear();
586 temp_bytes_parsed.clear();
587 current_message_raw.clear();
588 current_message_parsed.clear();
589
590 *current_pos = pos;
591 return message_data;
592 } else if (current_byte == 0xFF) {
593 return absl::InvalidArgumentError("message terminator not found");
594 }
595
596 temp_bytes_raw.push_back(current_byte);
597
598 // Check for command.
599 auto text_element = FindMatchingCommand(current_byte);
600 if (text_element != std::nullopt) {
601 temp_bytes_parsed.push_back(current_byte);
602 if (text_element->HasArgument) {
603 if (pos >= static_cast<int>(rom_data.size())) {
604 return absl::OutOfRangeError("message command argument out of range");
605 }
606 uint8_t arg_byte = rom_data[pos++];
607 temp_bytes_raw.push_back(arg_byte);
608 temp_bytes_parsed.push_back(arg_byte);
609 current_message_raw.append(text_element->GetParamToken(arg_byte));
610 current_message_parsed.append(text_element->GetParamToken(arg_byte));
611 } else {
612 current_message_raw.append(text_element->GetParamToken());
613 current_message_parsed.append(text_element->GetParamToken());
614 }
615 continue;
616 }
617
618 // Check for special characters.
619 if (auto special_element = FindMatchingSpecial(current_byte);
620 special_element != std::nullopt) {
621 current_message_raw.append(special_element->GetParamToken());
622 current_message_parsed.append(special_element->GetParamToken());
623 temp_bytes_parsed.push_back(current_byte);
624 continue;
625 }
626
627 // Check for dictionary.
628 int8_t dictionary = FindDictionaryEntry(current_byte);
629 if (dictionary >= 0) {
630 std::string token = absl::StrFormat(
631 "[%s:%02X]", DICTIONARYTOKEN, static_cast<unsigned char>(dictionary));
632 current_message_raw.append(token);
633 current_message_parsed.append(token);
634 temp_bytes_parsed.push_back(current_byte);
635 continue;
636 }
637
638 // Everything else.
639 if (CharEncoder.contains(current_byte)) {
640 std::string str = "";
641 str.push_back(CharEncoder.at(current_byte));
642 current_message_raw.append(str);
643 current_message_parsed.append(str);
644 temp_bytes_parsed.push_back(current_byte);
645 }
646 }
647
648 *current_pos = pos;
649 return absl::InvalidArgumentError("message terminator not found");
650}
651
652std::vector<std::string> ParseMessageData(
653 std::vector<MessageData>& message_data,
654 const std::vector<DictionaryEntry>& dictionary_entries) {
655 std::vector<std::string> parsed_messages;
656
657 for (auto& message : message_data) {
658 std::string parsed_message = "";
659 // Use index-based loop to properly skip argument bytes
660 for (size_t pos = 0; pos < message.Data.size(); ++pos) {
661 uint8_t byte = message.Data[pos];
662
663 // Check for text commands first (they may have arguments to skip)
664 auto text_element = FindMatchingCommand(byte);
665 if (text_element != std::nullopt) {
666 // Add newline for certain commands
667 if (text_element->ID == kScrollVertical || text_element->ID == kLine2 ||
668 text_element->ID == kLine3) {
669 parsed_message.append("\n");
670 }
671 // If command has an argument, get it from next byte and skip it
672 if (text_element->HasArgument && pos + 1 < message.Data.size()) {
673 uint8_t arg_byte = message.Data[pos + 1];
674 parsed_message.append(text_element->GetParamToken(arg_byte));
675 pos++; // Skip the argument byte
676 } else {
677 parsed_message.append(text_element->GetParamToken());
678 }
679 continue; // Move to next byte
680 }
681
682 // Check for special characters
683 auto special_element = FindMatchingSpecial(byte);
684 if (special_element != std::nullopt) {
685 parsed_message.append(special_element->GetParamToken());
686 continue;
687 }
688
689 // Check for dictionary entries
690 if (byte >= DICTOFF && byte < (DICTOFF + 97)) {
691 DictionaryEntry dic_entry;
692 for (const auto& entry : dictionary_entries) {
693 if (entry.ID == byte - DICTOFF) {
694 dic_entry = entry;
695 break;
696 }
697 }
698 parsed_message.append(dic_entry.Contents);
699 continue;
700 }
701
702 // Finally check for regular characters
703 if (CharEncoder.contains(byte)) {
704 parsed_message.push_back(CharEncoder.at(byte));
705 }
706 }
707 parsed_messages.push_back(parsed_message);
708 }
709
710 return parsed_messages;
711}
712
713std::vector<MessageData> ReadAllTextData(uint8_t* rom, int pos, int max_pos,
714 bool allow_bank_switch) {
715 std::vector<MessageData> list_of_texts;
716 int message_id = 0;
717
718 if (!rom) {
719 return list_of_texts;
720 }
721 if (max_pos > 0 && (pos < 0 || pos >= max_pos)) {
722 return list_of_texts;
723 }
724
725 std::vector<uint8_t> raw_message;
726 std::vector<uint8_t> parsed_message;
727 std::string current_raw_message;
728 std::string current_parsed_message;
729
730 bool did_bank_switch = false;
731 uint8_t current_byte = 0;
732 while (current_byte != 0xFF) {
733 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
734 break;
735 current_byte = rom[pos++];
736 if (current_byte == kMessageTerminator) {
737 list_of_texts.push_back(
738 MessageData(message_id++, pos, current_raw_message, raw_message,
739 current_parsed_message, parsed_message));
740 raw_message.clear();
741 parsed_message.clear();
742 current_raw_message.clear();
743 current_parsed_message.clear();
744 continue;
745 } else if (current_byte == 0xFF) {
746 break;
747 }
748
749 raw_message.push_back(current_byte);
750
751 auto text_element = FindMatchingCommand(current_byte);
752 if (text_element != std::nullopt) {
753 parsed_message.push_back(current_byte);
754 if (text_element->HasArgument) {
755 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
756 break;
757 current_byte = rom[pos++];
758 raw_message.push_back(current_byte);
759 parsed_message.push_back(current_byte);
760 }
761
762 current_raw_message.append(text_element->GetParamToken(current_byte));
763 current_parsed_message.append(text_element->GetParamToken(current_byte));
764
765 if (allow_bank_switch && text_element->Token == kBankToken &&
766 !did_bank_switch) {
767 did_bank_switch = true;
768 pos = kTextData2;
769 }
770
771 continue;
772 }
773
774 // Check for special characters.
775 auto special_element = FindMatchingSpecial(current_byte);
776 if (special_element != std::nullopt) {
777 current_raw_message.append(special_element->GetParamToken());
778 current_parsed_message.append(special_element->GetParamToken());
779 parsed_message.push_back(current_byte);
780 continue;
781 }
782
783 // Check for dictionary.
784 int8_t dictionary = FindDictionaryEntry(current_byte);
785 if (dictionary >= 0) {
786 current_raw_message.append(
787 absl::StrFormat("[%s:%02X]", DICTIONARYTOKEN,
788 static_cast<unsigned char>(dictionary)));
789
790 // Safety: bounds-check dictionary pointer reads and dictionary expansion.
791 // This parser is used by tooling (RomDoctor) that may run on dummy or
792 // partially-initialized ROM buffers.
793 const int ptr_a = kPointersDictionaries + (dictionary * 2);
794 const int ptr_b = kPointersDictionaries + ((dictionary + 1) * 2);
795 if (max_pos > 0) {
796 if (ptr_a < 0 || ptr_a + 1 >= max_pos || ptr_b < 0 ||
797 ptr_b + 1 >= max_pos) {
798 continue;
799 }
800 }
801
802 uint32_t address =
803 Get24LocalFromPC(rom, kPointersDictionaries + (dictionary * 2));
804 uint32_t address_end =
805 Get24LocalFromPC(rom, kPointersDictionaries + ((dictionary + 1) * 2));
806
807 if (max_pos > 0) {
808 const uint32_t max_u = static_cast<uint32_t>(max_pos);
809 if (address >= max_u || address_end > max_u || address_end < address) {
810 continue;
811 }
812 }
813
814 for (uint32_t i = address; i < address_end; i++) {
815 if (max_pos > 0 && i >= static_cast<uint32_t>(max_pos))
816 break;
817 parsed_message.push_back(rom[i]);
818 current_parsed_message.append(ParseTextDataByte(rom[i]));
819 }
820
821 continue;
822 }
823
824 // Everything else.
825 if (CharEncoder.contains(current_byte)) {
826 std::string str = "";
827 str.push_back(CharEncoder.at(current_byte));
828 current_raw_message.append(str);
829 current_parsed_message.append(str);
830 parsed_message.push_back(current_byte);
831 }
832 }
833
834 return list_of_texts;
835}
836
837absl::Status LoadExpandedMessages(std::string& expanded_message_path,
838 std::vector<std::string>& parsed_messages,
839 std::vector<MessageData>& expanded_messages,
840 std::vector<DictionaryEntry>& dictionary) {
841 static Rom expanded_message_rom;
842 if (!expanded_message_rom.LoadFromFile(expanded_message_path).ok()) {
843 return absl::InternalError("Failed to load expanded message ROM");
844 }
845 expanded_messages = ReadAllTextData(expanded_message_rom.mutable_data(), 0);
846 auto parsed_expanded_messages =
847 ParseMessageData(expanded_messages, dictionary);
848 // Insert into parsed_messages
849 for (const auto& expanded_message : expanded_messages) {
850 parsed_messages.push_back(parsed_expanded_messages[expanded_message.ID]);
851 }
852 return absl::OkStatus();
853}
854
856 const std::vector<MessageData>& messages) {
857 nlohmann::json j = nlohmann::json::array();
858 for (const auto& msg : messages) {
859 j.push_back({{"id", msg.ID},
860 {"address", msg.Address},
861 {"raw_string", msg.RawString},
862 {"parsed_string", msg.ContentsParsed}});
863 }
864 return j;
865}
866
867absl::Status ExportMessagesToJson(const std::string& path,
868 const std::vector<MessageData>& messages) {
869 try {
870 nlohmann::json j = SerializeMessagesToJson(messages);
871 std::ofstream file(path);
872 if (!file.is_open()) {
873 return absl::InternalError(
874 absl::StrFormat("Failed to open file for writing: %s", path));
875 }
876 file << j.dump(2); // Pretty print with 2-space indent
877 return absl::OkStatus();
878 } catch (const std::exception& e) {
879 return absl::InternalError(
880 absl::StrFormat("JSON export failed: %s", e.what()));
881 }
882}
883
885 const std::vector<MessageData>& vanilla,
886 const std::vector<MessageData>& expanded) {
887 nlohmann::json j;
888 j["format"] = "yaze-message-bundle";
889 j["version"] = kMessageBundleVersion;
890 j["counts"] = {{"vanilla", vanilla.size()}, {"expanded", expanded.size()}};
891 j["messages"] = nlohmann::json::array();
892
893 auto append_messages = [&j](const std::vector<MessageData>& messages,
894 MessageBank bank) {
895 for (const auto& msg : messages) {
896 nlohmann::json entry;
897 entry["id"] = msg.ID;
898 entry["bank"] = MessageBankToString(bank);
899 entry["address"] = msg.Address;
900 entry["raw"] = msg.RawString;
901 entry["parsed"] = msg.ContentsParsed;
902 entry["text"] =
903 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
904 entry["length"] = msg.Data.size();
905 const std::string validation_text =
906 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
907 auto warnings = ValidateMessageLineWidths(validation_text);
908 if (!warnings.empty()) {
909 entry["line_width_warnings"] = warnings;
910 }
911 j["messages"].push_back(entry);
912 }
913 };
914
915 append_messages(vanilla, MessageBank::kVanilla);
916 append_messages(expanded, MessageBank::kExpanded);
917
918 return j;
919}
920
922 const std::string& path, const std::vector<MessageData>& vanilla,
923 const std::vector<MessageData>& expanded) {
924 try {
925 nlohmann::json j = SerializeMessageBundle(vanilla, expanded);
926 std::ofstream file(path);
927 if (!file.is_open()) {
928 return absl::InternalError(
929 absl::StrFormat("Failed to open file for writing: %s", path));
930 }
931 file << j.dump(2);
932 return absl::OkStatus();
933 } catch (const std::exception& e) {
934 return absl::InternalError(
935 absl::StrFormat("Message bundle export failed: %s", e.what()));
936 }
937}
938
939namespace {
940absl::StatusOr<MessageBundleEntry> ParseMessageBundleEntry(
941 const nlohmann::json& entry, MessageBank default_bank) {
942 if (!entry.is_object()) {
943 return absl::InvalidArgumentError("Message entry must be an object");
944 }
945
946 MessageBundleEntry result;
947 result.id = entry.value("id", -1);
948 if (result.id < 0) {
949 return absl::InvalidArgumentError("Message entry missing valid id");
950 }
951
952 if (entry.contains("bank")) {
953 if (!entry["bank"].is_string()) {
954 return absl::InvalidArgumentError("Message entry bank must be string");
955 }
956 auto bank_or = MessageBankFromString(entry["bank"].get<std::string>());
957 if (!bank_or.ok()) {
958 return bank_or.status();
959 }
960 result.bank = bank_or.value();
961 } else {
962 result.bank = default_bank;
963 }
964
965 if (entry.contains("raw") && entry["raw"].is_string()) {
966 result.raw = entry["raw"].get<std::string>();
967 } else if (entry.contains("raw_string") && entry["raw_string"].is_string()) {
968 result.raw = entry["raw_string"].get<std::string>();
969 }
970
971 if (entry.contains("parsed") && entry["parsed"].is_string()) {
972 result.parsed = entry["parsed"].get<std::string>();
973 } else if (entry.contains("parsed_string") &&
974 entry["parsed_string"].is_string()) {
975 result.parsed = entry["parsed_string"].get<std::string>();
976 }
977
978 if (entry.contains("text") && entry["text"].is_string()) {
979 result.text = entry["text"].get<std::string>();
980 }
981
982 if (result.text.empty()) {
983 if (!result.raw.empty()) {
984 result.text = result.raw;
985 } else if (!result.parsed.empty()) {
986 result.text = result.parsed;
987 }
988 }
989
990 if (result.text.empty()) {
991 return absl::InvalidArgumentError(
992 absl::StrFormat("Message entry %d missing text content", result.id));
993 }
994
995 return result;
996}
997} // namespace
998
999absl::StatusOr<std::vector<MessageBundleEntry>> ParseMessageBundleJson(
1000 const nlohmann::json& json) {
1001 std::vector<MessageBundleEntry> entries;
1002
1003 if (json.is_array()) {
1004 for (const auto& entry : json) {
1005 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
1006 if (!parsed_or.ok()) {
1007 return parsed_or.status();
1008 }
1009 entries.push_back(parsed_or.value());
1010 }
1011 return entries;
1012 }
1013
1014 if (!json.is_object()) {
1015 return absl::InvalidArgumentError("Message bundle JSON must be object");
1016 }
1017
1018 if (json.contains("version") && json["version"].is_number_integer()) {
1019 int version = json["version"].get<int>();
1020 if (version != kMessageBundleVersion) {
1021 return absl::InvalidArgumentError(
1022 absl::StrFormat("Unsupported message bundle version: %d", version));
1023 }
1024 }
1025
1026 if (!json.contains("messages") || !json["messages"].is_array()) {
1027 return absl::InvalidArgumentError("Message bundle missing messages array");
1028 }
1029
1030 for (const auto& entry : json["messages"]) {
1031 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
1032 if (!parsed_or.ok()) {
1033 return parsed_or.status();
1034 }
1035 entries.push_back(parsed_or.value());
1036 }
1037
1038 return entries;
1039}
1040
1041absl::StatusOr<std::vector<MessageBundleEntry>> LoadMessageBundleFromJson(
1042 const std::string& path) {
1043 std::ifstream file(path);
1044 if (!file.is_open()) {
1045 return absl::NotFoundError(
1046 absl::StrFormat("Cannot open message bundle: %s", path));
1047 }
1048
1049 nlohmann::json json;
1050 try {
1051 file >> json;
1052 } catch (const std::exception& e) {
1053 return absl::InvalidArgumentError(
1054 absl::StrFormat("Failed to parse JSON: %s", e.what()));
1055 }
1056
1057 return ParseMessageBundleJson(json);
1058}
1059
1060// ===========================================================================
1061// Line Width Validation
1062// ===========================================================================
1063
1064std::vector<std::string> ValidateMessageLineWidths(const std::string& message) {
1065 std::vector<std::string> warnings;
1066
1067 // Split message into lines on line-break tokens: [1], [2], [3], [V], [K]
1068 // We walk through the string, counting visible characters per line.
1069 int line_num = 1;
1070 int visible_chars = 0;
1071 bool all_spaces_this_line = true;
1072 size_t pos = 0;
1073
1074 while (pos < message.size()) {
1075 if (message[pos] == '[') {
1076 // Find the closing bracket
1077 size_t close = message.find(']', pos);
1078 if (close == std::string::npos)
1079 break;
1080
1081 std::string token = message.substr(pos, close - pos + 1);
1082 pos = close + 1;
1083
1084 // Check if this token is a line-breaking command
1085 // Line breaks: [1], [2], [3], [V], [K]
1086 if (token == "[1]" || token == "[2]" || token == "[3]" ||
1087 token == "[V]" || token == "[K]") {
1088 // Check current line width before breaking.
1089 // Exempt whitespace-only lines (used as screen clears in ALTTP).
1090 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
1091 warnings.push_back(
1092 absl::StrFormat("Line %d: %d visible characters (max %d)",
1093 line_num, visible_chars, kMaxLineWidth));
1094 }
1095 line_num++;
1096 visible_chars = 0;
1097 all_spaces_this_line = true;
1098 }
1099 // Other command tokens ([W:02], [S:03], [SFX:2D], [L], [...], etc.)
1100 // are not counted as visible characters - they're control codes or
1101 // expand to game-rendered content that we can't measure in chars.
1102 // Exception: [L] expands to player name but width varies (1-6 chars).
1103 // For simplicity, we don't count command tokens.
1104 continue;
1105 }
1106
1107 // Regular visible character
1108 if (message[pos] != ' ')
1109 all_spaces_this_line = false;
1110 visible_chars++;
1111 pos++;
1112 }
1113
1114 // Check the last line (exempt whitespace-only lines)
1115 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
1116 warnings.push_back(
1117 absl::StrFormat("Line %d: %d visible characters (max %d)", line_num,
1118 visible_chars, kMaxLineWidth));
1119 }
1120
1121 return warnings;
1122}
1123
1124// ===========================================================================
1125// Org Format (.org) Import/Export
1126// ===========================================================================
1127
1128std::optional<std::pair<int, std::string>> ParseOrgHeader(
1129 const std::string& line) {
1130 // Expected format: "** XX - Label Text"
1131 // where XX is a hex message ID
1132 if (line.size() < 6 || line[0] != '*' || line[1] != '*' || line[2] != ' ') {
1133 return std::nullopt;
1134 }
1135
1136 // Find the " - " separator
1137 size_t sep = line.find(" - ", 3);
1138 if (sep == std::string::npos) {
1139 return std::nullopt;
1140 }
1141
1142 // Parse hex ID between "** " and " - "
1143 std::string hex_id = line.substr(3, sep - 3);
1144 int message_id;
1145 try {
1146 message_id = std::stoi(hex_id, nullptr, 16);
1147 } catch (const std::exception&) {
1148 return std::nullopt;
1149 }
1150
1151 // Extract label after " - "
1152 std::string label = line.substr(sep + 3);
1153
1154 return std::make_pair(message_id, label);
1155}
1156
1157std::vector<std::pair<int, std::string>> ParseOrgContent(
1158 const std::string& content) {
1159 std::vector<std::pair<int, std::string>> messages;
1160 std::istringstream stream(content);
1161 std::string line;
1162
1163 int current_id = -1;
1164 std::string current_body;
1165
1166 while (std::getline(stream, line)) {
1167 // Check if this is a header line
1168 auto header = ParseOrgHeader(line);
1169 if (header.has_value()) {
1170 // Save previous message if any
1171 if (current_id >= 0) {
1172 // Trim trailing newline from body
1173 while (!current_body.empty() && current_body.back() == '\n') {
1174 current_body.pop_back();
1175 }
1176 messages.push_back({current_id, current_body});
1177 }
1178
1179 current_id = header->first;
1180 current_body.clear();
1181 continue;
1182 }
1183
1184 // Skip top-level org headers (single *)
1185 if (!line.empty() && line[0] == '*' &&
1186 (line.size() < 2 || line[1] != '*')) {
1187 continue;
1188 }
1189
1190 // Accumulate body text
1191 if (current_id >= 0) {
1192 if (!current_body.empty()) {
1193 current_body += "\n";
1194 }
1195 current_body += line;
1196 }
1197 }
1198
1199 // Save last message
1200 if (current_id >= 0) {
1201 while (!current_body.empty() && current_body.back() == '\n') {
1202 current_body.pop_back();
1203 }
1204 messages.push_back({current_id, current_body});
1205 }
1206
1207 return messages;
1208}
1209
1211 const std::vector<std::pair<int, std::string>>& messages,
1212 const std::vector<std::string>& labels) {
1213 std::string output;
1214 output += "* Oracle of Secrets English Dialogue\n";
1215
1216 for (size_t i = 0; i < messages.size(); ++i) {
1217 const auto& [msg_id, body] = messages[i];
1218 std::string label = (i < labels.size())
1219 ? labels[i]
1220 : absl::StrFormat("Message %02X", msg_id);
1221
1222 output += absl::StrFormat("** %02X - %s\n", msg_id, label);
1223 output += body;
1224 output += "\n\n";
1225 }
1226
1227 return output;
1228}
1229
1230// ===========================================================================
1231// Expanded Message Bank
1232// ===========================================================================
1233
1234std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos) {
1235 // Expanded messages occupy one contiguous region. A vanilla [BANK] command
1236 // must not redirect parsing into the vanilla second text bank.
1237 return ReadAllTextData(rom, pos, /*max_pos=*/-1,
1238 /*allow_bank_switch=*/false);
1239}
1240
1241std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos, int end) {
1242 if (end < pos) {
1243 return {};
1244 }
1245 // ReadAllTextData's max_pos is exclusive; expanded-region ends are
1246 // configured as inclusive addresses.
1247 return ReadAllTextData(rom, pos, end + 1, /*allow_bank_switch=*/false);
1248}
1249
1250absl::Status WriteExpandedTextData(Rom* rom, int start, int end,
1251 const std::vector<std::string>& messages) {
1252 if (rom == nullptr || !rom->is_loaded()) {
1253 return absl::InvalidArgumentError("ROM not loaded");
1254 }
1255 if (start < 0 || end < start) {
1256 return absl::InvalidArgumentError("Invalid expanded message region");
1257 }
1258
1259 const int capacity = end - start + 1;
1260 if (capacity <= 0) {
1261 return absl::InvalidArgumentError(
1262 "Expanded message region has no capacity");
1263 }
1264
1265 const auto& data = rom->vector();
1266 if (end >= static_cast<int>(data.size())) {
1267 return absl::OutOfRangeError("Expanded message region out of ROM range");
1268 }
1269
1270 // Serialize into a contiguous buffer, then do a single ROM write for safety
1271 // and determinism (and to honor write fences).
1272 std::vector<uint8_t> blob;
1273 blob.reserve(static_cast<size_t>(capacity));
1274
1275 int used = 0;
1276 for (size_t i = 0; i < messages.size(); ++i) {
1277 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1278 if (!parsed.ok()) {
1279 return absl::InvalidArgumentError(
1280 absl::StrFormat("Expanded message %d is invalid: %s",
1281 static_cast<int>(i), parsed.errors.front()));
1282 }
1283 if (messages[i].find("[BANK]") != std::string::npos) {
1284 return absl::InvalidArgumentError(absl::StrFormat(
1285 "Expanded message %d contains [BANK], which is only valid in the "
1286 "vanilla message stream",
1287 static_cast<int>(i)));
1288 }
1289 auto bytes = std::move(parsed.bytes);
1290 const int needed = static_cast<int>(bytes.size()) + 1; // +0x7F
1291
1292 // Always reserve space for the final 0xFF.
1293 if (used + needed + 1 > capacity) {
1294 return absl::ResourceExhaustedError(absl::StrFormat(
1295 "Expanded message data exceeds bank boundary "
1296 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
1297 static_cast<int>(i), used, needed, capacity, end));
1298 }
1299
1300 blob.insert(blob.end(), bytes.begin(), bytes.end());
1301 blob.push_back(kMessageTerminator);
1302 used += needed;
1303 }
1304
1305 if (used + 1 > capacity) {
1306 return absl::ResourceExhaustedError(
1307 "No space for end-of-region marker (0xFF)");
1308 }
1309 blob.push_back(0xFF);
1310
1311 // ROM safety: this writer must only touch the expanded message region.
1312 // NOTE: `end` is inclusive; convert to half-open for the fence.
1314 const uint32_t fence_start = static_cast<uint32_t>(start);
1315 const uint32_t fence_end =
1316 static_cast<uint32_t>(static_cast<uint64_t>(end) + 1ULL);
1317 RETURN_IF_ERROR(fence.Allow(fence_start, fence_end, "ExpandedMessageBank"));
1318 yaze::rom::ScopedWriteFence scope(rom, &fence);
1319
1320 return rom->WriteVector(start, std::move(blob));
1321}
1322
1323absl::Status WriteExpandedTextData(uint8_t* rom, int start, int end,
1324 const std::vector<std::string>& messages) {
1325 if (rom == nullptr || start < 0 || end < start) {
1326 return absl::InvalidArgumentError("Invalid expanded message region");
1327 }
1328
1329 const int capacity = end - start + 1;
1330 int used = 0;
1331 std::vector<std::vector<uint8_t>> encoded_messages;
1332 encoded_messages.reserve(messages.size());
1333
1334 for (size_t i = 0; i < messages.size(); ++i) {
1335 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1336 if (!parsed.ok()) {
1337 return absl::InvalidArgumentError(
1338 absl::StrFormat("Expanded message %d is invalid: %s",
1339 static_cast<int>(i), parsed.errors.front()));
1340 }
1341 if (messages[i].find("[BANK]") != std::string::npos) {
1342 return absl::InvalidArgumentError(absl::StrFormat(
1343 "Expanded message %d contains [BANK], which is only valid in the "
1344 "vanilla message stream",
1345 static_cast<int>(i)));
1346 }
1347 auto bytes = std::move(parsed.bytes);
1348
1349 const int needed = static_cast<int>(bytes.size()) + 1; // +1 for 0x7F
1350 if (used + needed + 1 > capacity) {
1351 return absl::ResourceExhaustedError(
1352 absl::StrFormat("Expanded message data exceeds bank boundary "
1353 "(at message %d, used %d, end 0x%06X)",
1354 static_cast<int>(i), used, end));
1355 }
1356 used += needed;
1357 encoded_messages.push_back(std::move(bytes));
1358 }
1359
1360 int pos = start;
1361 for (const auto& bytes : encoded_messages) {
1362 for (uint8_t byte : bytes) {
1363 rom[pos++] = byte;
1364 }
1365 rom[pos++] = kMessageTerminator;
1366 }
1367
1368 rom[pos++] = 0xFF;
1369
1370 return absl::OkStatus();
1371}
1372
1373std::vector<std::pair<uint32_t, uint32_t>>
1375 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1376 ranges.reserve(writes_.size());
1377 for (const VanillaMessageWrite& write : writes_) {
1378 ranges.emplace_back(write.start(), write.end());
1379 }
1380 return ranges;
1381}
1382
1383std::vector<std::pair<uint32_t, uint32_t>>
1385 size_t rom_size) const {
1386 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1387 for (const VanillaMessageWrite& write : writes_) {
1388 const std::vector<uint8_t>& bytes = write.bytes();
1389 auto differs = [&](size_t offset) {
1390 const size_t pc = write.start() + offset;
1391 return rom == nullptr || pc >= rom_size || rom[pc] != bytes[offset];
1392 };
1393 size_t offset = 0;
1394 while (offset < bytes.size()) {
1395 if (!differs(offset)) {
1396 ++offset;
1397 continue;
1398 }
1399 size_t end = offset + 1;
1400 while (end < bytes.size() && differs(end)) {
1401 ++end;
1402 }
1403 ranges.emplace_back(write.start() + static_cast<uint32_t>(offset),
1404 write.start() + static_cast<uint32_t>(end));
1405 offset = end;
1406 }
1407 }
1408 return ranges;
1409}
1410
1411absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
1412 const std::vector<MessageData>& messages,
1413 std::optional<size_t> expected_message_count) {
1414 if (expected_message_count.has_value() &&
1415 messages.size() != *expected_message_count) {
1416 return absl::FailedPreconditionError(
1417 absl::StrFormat("Vanilla message count mismatch: expected %zu, got %zu",
1418 *expected_message_count, messages.size()));
1419 }
1420
1421 constexpr size_t kPrimaryCapacity = kTextDataEnd - kTextData + 1;
1422 constexpr size_t kSecondaryCapacity = kTextData2End - kTextData2 + 1;
1423 std::vector<uint8_t> primary;
1424 std::vector<uint8_t> secondary;
1425 primary.reserve(kPrimaryCapacity);
1426 secondary.reserve(kSecondaryCapacity);
1427
1428 bool in_second_bank = false;
1429 size_t bank_switch_count = 0;
1430 auto append_byte = [&](uint8_t value, size_t message_index) -> absl::Status {
1431 auto& destination = in_second_bank ? secondary : primary;
1432 const size_t capacity =
1433 in_second_bank ? kSecondaryCapacity : kPrimaryCapacity;
1434 if (destination.size() >= capacity) {
1435 const uint32_t address =
1436 static_cast<uint32_t>(in_second_bank ? kTextData2 : kTextData) +
1437 static_cast<uint32_t>(destination.size());
1438 return absl::ResourceExhaustedError(absl::StrFormat(
1439 "Vanilla message %zu exceeds %s bank capacity at 0x%06X",
1440 message_index, in_second_bank ? "second" : "first", address));
1441 }
1442 destination.push_back(value);
1443 return absl::OkStatus();
1444 };
1445
1446 for (size_t message_index = 0; message_index < messages.size();
1447 ++message_index) {
1448 bool next_byte_is_command_argument = false;
1449 for (uint8_t value : messages[message_index].Data) {
1450 const bool is_command_argument = next_byte_is_command_argument;
1451 next_byte_is_command_argument = false;
1452 if (!is_command_argument &&
1453 (value == kMessageTerminator || value == 0xFF)) {
1454 return absl::InvalidArgumentError(absl::StrFormat(
1455 "Vanilla message %zu contains reserved stream marker 0x%02X",
1456 message_index, value));
1457 }
1458 const bool is_bank_switch =
1459 !is_command_argument && value == kBankSwitchCommand;
1460 if (is_bank_switch && bank_switch_count != 0) {
1461 return absl::InvalidArgumentError(absl::StrFormat(
1462 "Vanilla message %zu contains a duplicate [BANK] command",
1463 message_index));
1464 }
1465
1466 RETURN_IF_ERROR(append_byte(value, message_index));
1467 if (is_bank_switch) {
1468 ++bank_switch_count;
1469 in_second_bank = true;
1470 } else if (!is_command_argument) {
1471 const auto command = FindMatchingCommand(value);
1472 next_byte_is_command_argument =
1473 command.has_value() && command->HasArgument;
1474 }
1475 }
1476 if (next_byte_is_command_argument) {
1477 return absl::InvalidArgumentError(absl::StrFormat(
1478 "Vanilla message %zu ends with a command missing its argument",
1479 message_index));
1480 }
1481 RETURN_IF_ERROR(append_byte(kMessageTerminator, message_index));
1482 }
1483
1484 if (bank_switch_count != 1) {
1485 return absl::InvalidArgumentError(
1486 "Vanilla message stream is missing the required [BANK] command");
1487 }
1488 RETURN_IF_ERROR(append_byte(0xFF, messages.size()));
1489
1490 std::vector<VanillaMessageWrite> writes;
1491 writes.reserve(in_second_bank ? 2 : 1);
1492 writes.emplace_back(static_cast<uint32_t>(kTextData), std::move(primary));
1493 if (in_second_bank) {
1494 writes.emplace_back(static_cast<uint32_t>(kTextData2),
1495 std::move(secondary));
1496 }
1497 return VanillaMessageSavePlan(std::move(writes), messages.size(),
1498 bank_switch_count);
1499}
1500
1502 const VanillaMessageSavePlan& plan) {
1503 if (rom == nullptr || !rom->is_loaded()) {
1504 return absl::InvalidArgumentError("ROM not loaded");
1505 }
1506
1508 for (const VanillaMessageWrite& write : plan.writes()) {
1509 if (write.end() > rom->size()) {
1510 return absl::OutOfRangeError(absl::StrFormat(
1511 "Vanilla message write range [0x%06X, 0x%06X) is outside the ROM",
1512 write.start(), write.end()));
1513 }
1515 fence.Allow(write.start(), write.end(), "VanillaMessageBank"));
1516 }
1517
1518 ScopedRomTransaction transaction(*rom);
1519 yaze::rom::ScopedWriteFence fence_scope(rom, &fence);
1520 for (const VanillaMessageWrite& write : plan.writes()) {
1522 rom->WriteVector(static_cast<int>(write.start()), write.bytes()));
1523 }
1524 transaction.Commit();
1525 return absl::OkStatus();
1526}
1527
1528absl::Status WriteAllTextData(Rom* rom,
1529 const std::vector<MessageData>& messages) {
1531 const VanillaMessageSavePlan plan,
1532 BuildVanillaMessageSavePlan(messages, /*expected_message_count=*/{}));
1533 return ApplyVanillaMessageSavePlan(rom, plan);
1534}
1535
1536} // namespace editor
1537} // 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:49
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:491
auto mutable_data()
Definition rom.h:202
const auto & vector() const
Definition rom.h:205
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:928
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
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 > > ChangedRanges(const uint8_t *rom, size_t rom_size) 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)
bool IsWordChar(std::string_view text, size_t index)
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)
std::vector< uint8_t > CompressMessageWithDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &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::vector< uint8_t > ExpandMessageDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &dictionary)
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::vector< uint8_t > > ReadDictionaryEntryBytes(const uint8_t *rom, size_t rom_size)
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:146
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