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 const auto dictionary_element =
202 TextElement(0x80, DICTIONARYTOKEN, true, "Dictionary");
203
204 if (!parsedElement.Active) {
205 util::logf("Error parsing message: %s", temp_string);
206 break;
207 } else if (parsedElement.Parent == dictionary_element) {
208 bytes.push_back(parsedElement.Value);
209 } else {
210 bytes.push_back(parsedElement.Parent.ID);
211
212 if (parsedElement.Parent.HasArgument) {
213 bytes.push_back(parsedElement.Value);
214 }
215 }
216
217 pos = next + 1;
218 continue;
219 } else {
220 uint8_t bb = FindMatchingCharacter(temp_string[pos++]);
221
222 if (bb != 0xFF) {
223 bytes.push_back(bb);
224 }
225 }
226 }
227
228 return bytes;
229}
230
232 MessageParseResult result;
233 std::string temp_string(str);
234 size_t pos = 0;
235 bool warned_newline = false;
236
237 while (pos < temp_string.size()) {
238 char current = temp_string[pos];
239 if (current == '\r' || current == '\n') {
240 if (!warned_newline) {
241 result.warnings.push_back(
242 "Literal newlines are ignored; use [1], [2], [3], [V], or [K] "
243 "tokens for line breaks.");
244 warned_newline = true;
245 }
246 pos++;
247 continue;
248 }
249
250 if (current == '[') {
251 size_t close = temp_string.find(']', pos);
252 if (close == std::string::npos) {
253 result.errors.push_back(
254 absl::StrFormat("Unclosed token starting at position %zu", pos));
255 break;
256 }
257
258 std::string token = temp_string.substr(pos, close - pos + 1);
259 ParsedElement parsed_element = FindMatchingElement(token);
260 const auto dictionary_element =
261 TextElement(0x80, DICTIONARYTOKEN, true, "Dictionary");
262
263 if (!parsed_element.Active) {
264 result.errors.push_back(absl::StrFormat("Unknown token: %s", token));
265 pos = close + 1;
266 continue;
267 }
268
269 if (!parsed_element.Parent.HasArgument) {
270 if (token != parsed_element.Parent.GetParamToken()) {
271 result.errors.push_back(absl::StrFormat("Unknown token: %s", token));
272 pos = close + 1;
273 continue;
274 }
275 }
276
277 if (parsed_element.Parent == dictionary_element) {
278 result.bytes.push_back(parsed_element.Value);
279 } else {
280 result.bytes.push_back(parsed_element.Parent.ID);
281 if (parsed_element.Parent.HasArgument) {
282 result.bytes.push_back(parsed_element.Value);
283 }
284 }
285
286 pos = close + 1;
287 continue;
288 }
289
290 uint8_t bb = FindMatchingCharacter(current);
291 if (bb == 0xFF) {
292 result.errors.push_back(absl::StrFormat(
293 "Unsupported character '%c' at position %zu", current, pos));
294 pos++;
295 continue;
296 }
297
298 result.bytes.push_back(bb);
299 pos++;
300 }
301
302 return result;
303}
304
306 switch (bank) {
308 return "vanilla";
310 return "expanded";
311 }
312 return "vanilla";
313}
314
315absl::StatusOr<MessageBank> MessageBankFromString(std::string_view value) {
316 const std::string lowered = absl::AsciiStrToLower(std::string(value));
317 if (lowered == "vanilla") {
319 }
320 if (lowered == "expanded") {
322 }
323 return absl::InvalidArgumentError(
324 absl::StrFormat("Unknown message bank: %s", std::string(value)));
325}
326
327std::vector<DictionaryEntry> BuildDictionaryEntries(Rom* rom) {
328 std::vector<DictionaryEntry> AllDictionaries;
329 for (int i = 0; i < kNumDictionaryEntries; i++) {
330 std::vector<uint8_t> bytes;
331 std::stringstream stringBuilder;
332
333 int address = SnesToPc(
334 kTextData + (rom->data()[kPointersDictionaries + (i * 2) + 1] << 8) +
335 rom->data()[kPointersDictionaries + (i * 2)]);
336
337 int temppush_backress =
339 (rom->data()[kPointersDictionaries + ((i + 1) * 2) + 1] << 8) +
340 rom->data()[kPointersDictionaries + ((i + 1) * 2)]);
341
342 while (address < temppush_backress) {
343 uint8_t uint8_tDictionary = rom->data()[address++];
344 bytes.push_back(uint8_tDictionary);
345 stringBuilder << ParseTextDataByte(uint8_tDictionary);
346 }
347
348 AllDictionaries.push_back(DictionaryEntry{(uint8_t)i, stringBuilder.str()});
349 }
350
351 std::ranges::sort(AllDictionaries,
352 [](const DictionaryEntry& a, const DictionaryEntry& b) {
353 return a.Contents.size() > b.Contents.size();
354 });
355
356 return AllDictionaries;
357}
358
360 std::string str, const std::vector<DictionaryEntry>& dictionary) {
361 std::string temp = std::move(str);
362 for (const auto& entry : dictionary) {
363 if (entry.ContainedInString(temp)) {
364 temp = entry.ReplaceInstancesOfIn(temp);
365 }
366 }
367 return temp;
368}
369
370std::optional<size_t> FindTextMatch(std::string_view text,
371 std::string_view query, size_t start_pos,
372 bool case_sensitive,
373 bool match_whole_word) {
374 if (query.empty() || start_pos > text.size()) {
375 return std::nullopt;
376 }
377
378 std::string haystack_storage;
379 std::string query_storage;
380 std::string_view haystack = text;
381 std::string_view needle = query;
382 if (!case_sensitive) {
383 haystack_storage = LowercaseCopy(text);
384 query_storage = LowercaseCopy(query);
385 haystack = haystack_storage;
386 needle = query_storage;
387 }
388
389 size_t pos = haystack.find(needle, start_pos);
390 while (pos != std::string::npos) {
391 if (!match_whole_word || MatchesWholeWordAt(text, pos, query.size())) {
392 return pos;
393 }
394 pos = haystack.find(needle, pos + 1);
395 }
396
397 return std::nullopt;
398}
399
400int ReplaceTextMatches(std::string* text, std::string_view query,
401 std::string_view replacement, size_t start_pos,
402 bool replace_all, bool case_sensitive,
403 bool match_whole_word, size_t* first_replaced_pos) {
404 if (!text || query.empty() || start_pos > text->size()) {
405 return 0;
406 }
407
408 int replacements = 0;
409 size_t cursor = start_pos;
410 while (true) {
411 const auto match_pos =
412 FindTextMatch(*text, query, cursor, case_sensitive, match_whole_word);
413 if (!match_pos.has_value()) {
414 break;
415 }
416
417 text->replace(*match_pos, query.size(), replacement);
418 if (replacements == 0 && first_replaced_pos != nullptr) {
419 *first_replaced_pos = *match_pos;
420 }
421 replacements++;
422
423 cursor = *match_pos + replacement.size();
424 if (!replace_all) {
425 break;
426 }
427
428 if (cursor > text->size()) {
429 break;
430 }
431 }
432
433 return replacements;
434}
435
437 uint8_t value, const std::vector<DictionaryEntry>& dictionary) {
438 for (const auto& entry : dictionary) {
439 if (entry.ID + DICTOFF == value) {
440 return entry;
441 }
442 }
443 return DictionaryEntry();
444}
445
446absl::StatusOr<MessageData> ParseSingleMessage(
447 const std::vector<uint8_t>& rom_data, int* current_pos) {
448 if (current_pos == nullptr) {
449 return absl::InvalidArgumentError("current_pos is null");
450 }
451 if (*current_pos < 0 ||
452 static_cast<size_t>(*current_pos) >= rom_data.size()) {
453 return absl::OutOfRangeError("current_pos is out of range");
454 }
455
456 MessageData message_data;
457 int pos = *current_pos;
458 uint8_t current_byte;
459 std::vector<uint8_t> temp_bytes_raw;
460 std::vector<uint8_t> temp_bytes_parsed;
461 std::string current_message_raw;
462 std::string current_message_parsed;
463
464 // Read the message data
465 while (pos < static_cast<int>(rom_data.size())) {
466 current_byte = rom_data[pos++];
467
468 if (current_byte == kMessageTerminator) {
469 message_data.ID = message_data.ID + 1;
470 message_data.Address = pos;
471 message_data.RawString = current_message_raw;
472 message_data.Data = temp_bytes_raw;
473 message_data.DataParsed = temp_bytes_parsed;
474 message_data.ContentsParsed = current_message_parsed;
475
476 temp_bytes_raw.clear();
477 temp_bytes_parsed.clear();
478 current_message_raw.clear();
479 current_message_parsed.clear();
480
481 *current_pos = pos;
482 return message_data;
483 } else if (current_byte == 0xFF) {
484 return absl::InvalidArgumentError("message terminator not found");
485 }
486
487 temp_bytes_raw.push_back(current_byte);
488
489 // Check for command.
490 auto text_element = FindMatchingCommand(current_byte);
491 if (text_element != std::nullopt) {
492 temp_bytes_parsed.push_back(current_byte);
493 if (text_element->HasArgument) {
494 if (pos >= static_cast<int>(rom_data.size())) {
495 return absl::OutOfRangeError("message command argument out of range");
496 }
497 uint8_t arg_byte = rom_data[pos++];
498 temp_bytes_raw.push_back(arg_byte);
499 temp_bytes_parsed.push_back(arg_byte);
500 current_message_raw.append(text_element->GetParamToken(arg_byte));
501 current_message_parsed.append(text_element->GetParamToken(arg_byte));
502 } else {
503 current_message_raw.append(text_element->GetParamToken());
504 current_message_parsed.append(text_element->GetParamToken());
505 }
506 continue;
507 }
508
509 // Check for special characters.
510 if (auto special_element = FindMatchingSpecial(current_byte);
511 special_element != std::nullopt) {
512 current_message_raw.append(special_element->GetParamToken());
513 current_message_parsed.append(special_element->GetParamToken());
514 temp_bytes_parsed.push_back(current_byte);
515 continue;
516 }
517
518 // Check for dictionary.
519 int8_t dictionary = FindDictionaryEntry(current_byte);
520 if (dictionary >= 0) {
521 std::string token = absl::StrFormat(
522 "[%s:%02X]", DICTIONARYTOKEN, static_cast<unsigned char>(dictionary));
523 current_message_raw.append(token);
524 current_message_parsed.append(token);
525 temp_bytes_parsed.push_back(current_byte);
526 continue;
527 }
528
529 // Everything else.
530 if (CharEncoder.contains(current_byte)) {
531 std::string str = "";
532 str.push_back(CharEncoder.at(current_byte));
533 current_message_raw.append(str);
534 current_message_parsed.append(str);
535 temp_bytes_parsed.push_back(current_byte);
536 }
537 }
538
539 *current_pos = pos;
540 return absl::InvalidArgumentError("message terminator not found");
541}
542
543std::vector<std::string> ParseMessageData(
544 std::vector<MessageData>& message_data,
545 const std::vector<DictionaryEntry>& dictionary_entries) {
546 std::vector<std::string> parsed_messages;
547
548 for (auto& message : message_data) {
549 std::string parsed_message = "";
550 // Use index-based loop to properly skip argument bytes
551 for (size_t pos = 0; pos < message.Data.size(); ++pos) {
552 uint8_t byte = message.Data[pos];
553
554 // Check for text commands first (they may have arguments to skip)
555 auto text_element = FindMatchingCommand(byte);
556 if (text_element != std::nullopt) {
557 // Add newline for certain commands
558 if (text_element->ID == kScrollVertical || text_element->ID == kLine2 ||
559 text_element->ID == kLine3) {
560 parsed_message.append("\n");
561 }
562 // If command has an argument, get it from next byte and skip it
563 if (text_element->HasArgument && pos + 1 < message.Data.size()) {
564 uint8_t arg_byte = message.Data[pos + 1];
565 parsed_message.append(text_element->GetParamToken(arg_byte));
566 pos++; // Skip the argument byte
567 } else {
568 parsed_message.append(text_element->GetParamToken());
569 }
570 continue; // Move to next byte
571 }
572
573 // Check for special characters
574 auto special_element = FindMatchingSpecial(byte);
575 if (special_element != std::nullopt) {
576 parsed_message.append(special_element->GetParamToken());
577 continue;
578 }
579
580 // Check for dictionary entries
581 if (byte >= DICTOFF && byte < (DICTOFF + 97)) {
582 DictionaryEntry dic_entry;
583 for (const auto& entry : dictionary_entries) {
584 if (entry.ID == byte - DICTOFF) {
585 dic_entry = entry;
586 break;
587 }
588 }
589 parsed_message.append(dic_entry.Contents);
590 continue;
591 }
592
593 // Finally check for regular characters
594 if (CharEncoder.contains(byte)) {
595 parsed_message.push_back(CharEncoder.at(byte));
596 }
597 }
598 parsed_messages.push_back(parsed_message);
599 }
600
601 return parsed_messages;
602}
603
604std::vector<MessageData> ReadAllTextData(uint8_t* rom, int pos, int max_pos,
605 bool allow_bank_switch) {
606 std::vector<MessageData> list_of_texts;
607 int message_id = 0;
608
609 if (!rom) {
610 return list_of_texts;
611 }
612 if (max_pos > 0 && (pos < 0 || pos >= max_pos)) {
613 return list_of_texts;
614 }
615
616 std::vector<uint8_t> raw_message;
617 std::vector<uint8_t> parsed_message;
618 std::string current_raw_message;
619 std::string current_parsed_message;
620
621 bool did_bank_switch = false;
622 uint8_t current_byte = 0;
623 while (current_byte != 0xFF) {
624 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
625 break;
626 current_byte = rom[pos++];
627 if (current_byte == kMessageTerminator) {
628 list_of_texts.push_back(
629 MessageData(message_id++, pos, current_raw_message, raw_message,
630 current_parsed_message, parsed_message));
631 raw_message.clear();
632 parsed_message.clear();
633 current_raw_message.clear();
634 current_parsed_message.clear();
635 continue;
636 } else if (current_byte == 0xFF) {
637 break;
638 }
639
640 raw_message.push_back(current_byte);
641
642 auto text_element = FindMatchingCommand(current_byte);
643 if (text_element != std::nullopt) {
644 parsed_message.push_back(current_byte);
645 if (text_element->HasArgument) {
646 if (max_pos > 0 && (pos < 0 || pos >= max_pos))
647 break;
648 current_byte = rom[pos++];
649 raw_message.push_back(current_byte);
650 parsed_message.push_back(current_byte);
651 }
652
653 current_raw_message.append(text_element->GetParamToken(current_byte));
654 current_parsed_message.append(text_element->GetParamToken(current_byte));
655
656 if (allow_bank_switch && text_element->Token == kBankToken &&
657 !did_bank_switch) {
658 did_bank_switch = true;
659 pos = kTextData2;
660 }
661
662 continue;
663 }
664
665 // Check for special characters.
666 auto special_element = FindMatchingSpecial(current_byte);
667 if (special_element != std::nullopt) {
668 current_raw_message.append(special_element->GetParamToken());
669 current_parsed_message.append(special_element->GetParamToken());
670 parsed_message.push_back(current_byte);
671 continue;
672 }
673
674 // Check for dictionary.
675 int8_t dictionary = FindDictionaryEntry(current_byte);
676 if (dictionary >= 0) {
677 current_raw_message.append(
678 absl::StrFormat("[%s:%02X]", DICTIONARYTOKEN,
679 static_cast<unsigned char>(dictionary)));
680
681 // Safety: bounds-check dictionary pointer reads and dictionary expansion.
682 // This parser is used by tooling (RomDoctor) that may run on dummy or
683 // partially-initialized ROM buffers.
684 const int ptr_a = kPointersDictionaries + (dictionary * 2);
685 const int ptr_b = kPointersDictionaries + ((dictionary + 1) * 2);
686 if (max_pos > 0) {
687 if (ptr_a < 0 || ptr_a + 1 >= max_pos || ptr_b < 0 ||
688 ptr_b + 1 >= max_pos) {
689 continue;
690 }
691 }
692
693 uint32_t address =
694 Get24LocalFromPC(rom, kPointersDictionaries + (dictionary * 2));
695 uint32_t address_end =
696 Get24LocalFromPC(rom, kPointersDictionaries + ((dictionary + 1) * 2));
697
698 if (max_pos > 0) {
699 const uint32_t max_u = static_cast<uint32_t>(max_pos);
700 if (address >= max_u || address_end > max_u || address_end < address) {
701 continue;
702 }
703 }
704
705 for (uint32_t i = address; i < address_end; i++) {
706 if (max_pos > 0 && i >= static_cast<uint32_t>(max_pos))
707 break;
708 parsed_message.push_back(rom[i]);
709 current_parsed_message.append(ParseTextDataByte(rom[i]));
710 }
711
712 continue;
713 }
714
715 // Everything else.
716 if (CharEncoder.contains(current_byte)) {
717 std::string str = "";
718 str.push_back(CharEncoder.at(current_byte));
719 current_raw_message.append(str);
720 current_parsed_message.append(str);
721 parsed_message.push_back(current_byte);
722 }
723 }
724
725 return list_of_texts;
726}
727
728absl::Status LoadExpandedMessages(std::string& expanded_message_path,
729 std::vector<std::string>& parsed_messages,
730 std::vector<MessageData>& expanded_messages,
731 std::vector<DictionaryEntry>& dictionary) {
732 static Rom expanded_message_rom;
733 if (!expanded_message_rom.LoadFromFile(expanded_message_path).ok()) {
734 return absl::InternalError("Failed to load expanded message ROM");
735 }
736 expanded_messages = ReadAllTextData(expanded_message_rom.mutable_data(), 0);
737 auto parsed_expanded_messages =
738 ParseMessageData(expanded_messages, dictionary);
739 // Insert into parsed_messages
740 for (const auto& expanded_message : expanded_messages) {
741 parsed_messages.push_back(parsed_expanded_messages[expanded_message.ID]);
742 }
743 return absl::OkStatus();
744}
745
747 const std::vector<MessageData>& messages) {
748 nlohmann::json j = nlohmann::json::array();
749 for (const auto& msg : messages) {
750 j.push_back({{"id", msg.ID},
751 {"address", msg.Address},
752 {"raw_string", msg.RawString},
753 {"parsed_string", msg.ContentsParsed}});
754 }
755 return j;
756}
757
758absl::Status ExportMessagesToJson(const std::string& path,
759 const std::vector<MessageData>& messages) {
760 try {
761 nlohmann::json j = SerializeMessagesToJson(messages);
762 std::ofstream file(path);
763 if (!file.is_open()) {
764 return absl::InternalError(
765 absl::StrFormat("Failed to open file for writing: %s", path));
766 }
767 file << j.dump(2); // Pretty print with 2-space indent
768 return absl::OkStatus();
769 } catch (const std::exception& e) {
770 return absl::InternalError(
771 absl::StrFormat("JSON export failed: %s", e.what()));
772 }
773}
774
776 const std::vector<MessageData>& vanilla,
777 const std::vector<MessageData>& expanded) {
778 nlohmann::json j;
779 j["format"] = "yaze-message-bundle";
780 j["version"] = kMessageBundleVersion;
781 j["counts"] = {{"vanilla", vanilla.size()}, {"expanded", expanded.size()}};
782 j["messages"] = nlohmann::json::array();
783
784 auto append_messages = [&j](const std::vector<MessageData>& messages,
785 MessageBank bank) {
786 for (const auto& msg : messages) {
787 nlohmann::json entry;
788 entry["id"] = msg.ID;
789 entry["bank"] = MessageBankToString(bank);
790 entry["address"] = msg.Address;
791 entry["raw"] = msg.RawString;
792 entry["parsed"] = msg.ContentsParsed;
793 entry["text"] =
794 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
795 entry["length"] = msg.Data.size();
796 const std::string validation_text =
797 !msg.RawString.empty() ? msg.RawString : msg.ContentsParsed;
798 auto warnings = ValidateMessageLineWidths(validation_text);
799 if (!warnings.empty()) {
800 entry["line_width_warnings"] = warnings;
801 }
802 j["messages"].push_back(entry);
803 }
804 };
805
806 append_messages(vanilla, MessageBank::kVanilla);
807 append_messages(expanded, MessageBank::kExpanded);
808
809 return j;
810}
811
813 const std::string& path, const std::vector<MessageData>& vanilla,
814 const std::vector<MessageData>& expanded) {
815 try {
816 nlohmann::json j = SerializeMessageBundle(vanilla, expanded);
817 std::ofstream file(path);
818 if (!file.is_open()) {
819 return absl::InternalError(
820 absl::StrFormat("Failed to open file for writing: %s", path));
821 }
822 file << j.dump(2);
823 return absl::OkStatus();
824 } catch (const std::exception& e) {
825 return absl::InternalError(
826 absl::StrFormat("Message bundle export failed: %s", e.what()));
827 }
828}
829
830namespace {
831absl::StatusOr<MessageBundleEntry> ParseMessageBundleEntry(
832 const nlohmann::json& entry, MessageBank default_bank) {
833 if (!entry.is_object()) {
834 return absl::InvalidArgumentError("Message entry must be an object");
835 }
836
837 MessageBundleEntry result;
838 result.id = entry.value("id", -1);
839 if (result.id < 0) {
840 return absl::InvalidArgumentError("Message entry missing valid id");
841 }
842
843 if (entry.contains("bank")) {
844 if (!entry["bank"].is_string()) {
845 return absl::InvalidArgumentError("Message entry bank must be string");
846 }
847 auto bank_or = MessageBankFromString(entry["bank"].get<std::string>());
848 if (!bank_or.ok()) {
849 return bank_or.status();
850 }
851 result.bank = bank_or.value();
852 } else {
853 result.bank = default_bank;
854 }
855
856 if (entry.contains("raw") && entry["raw"].is_string()) {
857 result.raw = entry["raw"].get<std::string>();
858 } else if (entry.contains("raw_string") && entry["raw_string"].is_string()) {
859 result.raw = entry["raw_string"].get<std::string>();
860 }
861
862 if (entry.contains("parsed") && entry["parsed"].is_string()) {
863 result.parsed = entry["parsed"].get<std::string>();
864 } else if (entry.contains("parsed_string") &&
865 entry["parsed_string"].is_string()) {
866 result.parsed = entry["parsed_string"].get<std::string>();
867 }
868
869 if (entry.contains("text") && entry["text"].is_string()) {
870 result.text = entry["text"].get<std::string>();
871 }
872
873 if (result.text.empty()) {
874 if (!result.raw.empty()) {
875 result.text = result.raw;
876 } else if (!result.parsed.empty()) {
877 result.text = result.parsed;
878 }
879 }
880
881 if (result.text.empty()) {
882 return absl::InvalidArgumentError(
883 absl::StrFormat("Message entry %d missing text content", result.id));
884 }
885
886 return result;
887}
888} // namespace
889
890absl::StatusOr<std::vector<MessageBundleEntry>> ParseMessageBundleJson(
891 const nlohmann::json& json) {
892 std::vector<MessageBundleEntry> entries;
893
894 if (json.is_array()) {
895 for (const auto& entry : json) {
896 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
897 if (!parsed_or.ok()) {
898 return parsed_or.status();
899 }
900 entries.push_back(parsed_or.value());
901 }
902 return entries;
903 }
904
905 if (!json.is_object()) {
906 return absl::InvalidArgumentError("Message bundle JSON must be object");
907 }
908
909 if (json.contains("version") && json["version"].is_number_integer()) {
910 int version = json["version"].get<int>();
911 if (version != kMessageBundleVersion) {
912 return absl::InvalidArgumentError(
913 absl::StrFormat("Unsupported message bundle version: %d", version));
914 }
915 }
916
917 if (!json.contains("messages") || !json["messages"].is_array()) {
918 return absl::InvalidArgumentError("Message bundle missing messages array");
919 }
920
921 for (const auto& entry : json["messages"]) {
922 auto parsed_or = ParseMessageBundleEntry(entry, MessageBank::kVanilla);
923 if (!parsed_or.ok()) {
924 return parsed_or.status();
925 }
926 entries.push_back(parsed_or.value());
927 }
928
929 return entries;
930}
931
932absl::StatusOr<std::vector<MessageBundleEntry>> LoadMessageBundleFromJson(
933 const std::string& path) {
934 std::ifstream file(path);
935 if (!file.is_open()) {
936 return absl::NotFoundError(
937 absl::StrFormat("Cannot open message bundle: %s", path));
938 }
939
940 nlohmann::json json;
941 try {
942 file >> json;
943 } catch (const std::exception& e) {
944 return absl::InvalidArgumentError(
945 absl::StrFormat("Failed to parse JSON: %s", e.what()));
946 }
947
948 return ParseMessageBundleJson(json);
949}
950
951// ===========================================================================
952// Line Width Validation
953// ===========================================================================
954
955std::vector<std::string> ValidateMessageLineWidths(const std::string& message) {
956 std::vector<std::string> warnings;
957
958 // Split message into lines on line-break tokens: [1], [2], [3], [V], [K]
959 // We walk through the string, counting visible characters per line.
960 int line_num = 1;
961 int visible_chars = 0;
962 bool all_spaces_this_line = true;
963 size_t pos = 0;
964
965 while (pos < message.size()) {
966 if (message[pos] == '[') {
967 // Find the closing bracket
968 size_t close = message.find(']', pos);
969 if (close == std::string::npos)
970 break;
971
972 std::string token = message.substr(pos, close - pos + 1);
973 pos = close + 1;
974
975 // Check if this token is a line-breaking command
976 // Line breaks: [1], [2], [3], [V], [K]
977 if (token == "[1]" || token == "[2]" || token == "[3]" ||
978 token == "[V]" || token == "[K]") {
979 // Check current line width before breaking.
980 // Exempt whitespace-only lines (used as screen clears in ALTTP).
981 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
982 warnings.push_back(
983 absl::StrFormat("Line %d: %d visible characters (max %d)",
984 line_num, visible_chars, kMaxLineWidth));
985 }
986 line_num++;
987 visible_chars = 0;
988 all_spaces_this_line = true;
989 }
990 // Other command tokens ([W:02], [S:03], [SFX:2D], [L], [...], etc.)
991 // are not counted as visible characters - they're control codes or
992 // expand to game-rendered content that we can't measure in chars.
993 // Exception: [L] expands to player name but width varies (1-6 chars).
994 // For simplicity, we don't count command tokens.
995 continue;
996 }
997
998 // Regular visible character
999 if (message[pos] != ' ')
1000 all_spaces_this_line = false;
1001 visible_chars++;
1002 pos++;
1003 }
1004
1005 // Check the last line (exempt whitespace-only lines)
1006 if (visible_chars > kMaxLineWidth && !all_spaces_this_line) {
1007 warnings.push_back(
1008 absl::StrFormat("Line %d: %d visible characters (max %d)", line_num,
1009 visible_chars, kMaxLineWidth));
1010 }
1011
1012 return warnings;
1013}
1014
1015// ===========================================================================
1016// Org Format (.org) Import/Export
1017// ===========================================================================
1018
1019std::optional<std::pair<int, std::string>> ParseOrgHeader(
1020 const std::string& line) {
1021 // Expected format: "** XX - Label Text"
1022 // where XX is a hex message ID
1023 if (line.size() < 6 || line[0] != '*' || line[1] != '*' || line[2] != ' ') {
1024 return std::nullopt;
1025 }
1026
1027 // Find the " - " separator
1028 size_t sep = line.find(" - ", 3);
1029 if (sep == std::string::npos) {
1030 return std::nullopt;
1031 }
1032
1033 // Parse hex ID between "** " and " - "
1034 std::string hex_id = line.substr(3, sep - 3);
1035 int message_id;
1036 try {
1037 message_id = std::stoi(hex_id, nullptr, 16);
1038 } catch (const std::exception&) {
1039 return std::nullopt;
1040 }
1041
1042 // Extract label after " - "
1043 std::string label = line.substr(sep + 3);
1044
1045 return std::make_pair(message_id, label);
1046}
1047
1048std::vector<std::pair<int, std::string>> ParseOrgContent(
1049 const std::string& content) {
1050 std::vector<std::pair<int, std::string>> messages;
1051 std::istringstream stream(content);
1052 std::string line;
1053
1054 int current_id = -1;
1055 std::string current_body;
1056
1057 while (std::getline(stream, line)) {
1058 // Check if this is a header line
1059 auto header = ParseOrgHeader(line);
1060 if (header.has_value()) {
1061 // Save previous message if any
1062 if (current_id >= 0) {
1063 // Trim trailing newline from body
1064 while (!current_body.empty() && current_body.back() == '\n') {
1065 current_body.pop_back();
1066 }
1067 messages.push_back({current_id, current_body});
1068 }
1069
1070 current_id = header->first;
1071 current_body.clear();
1072 continue;
1073 }
1074
1075 // Skip top-level org headers (single *)
1076 if (!line.empty() && line[0] == '*' &&
1077 (line.size() < 2 || line[1] != '*')) {
1078 continue;
1079 }
1080
1081 // Accumulate body text
1082 if (current_id >= 0) {
1083 if (!current_body.empty()) {
1084 current_body += "\n";
1085 }
1086 current_body += line;
1087 }
1088 }
1089
1090 // Save last message
1091 if (current_id >= 0) {
1092 while (!current_body.empty() && current_body.back() == '\n') {
1093 current_body.pop_back();
1094 }
1095 messages.push_back({current_id, current_body});
1096 }
1097
1098 return messages;
1099}
1100
1102 const std::vector<std::pair<int, std::string>>& messages,
1103 const std::vector<std::string>& labels) {
1104 std::string output;
1105 output += "* Oracle of Secrets English Dialogue\n";
1106
1107 for (size_t i = 0; i < messages.size(); ++i) {
1108 const auto& [msg_id, body] = messages[i];
1109 std::string label = (i < labels.size())
1110 ? labels[i]
1111 : absl::StrFormat("Message %02X", msg_id);
1112
1113 output += absl::StrFormat("** %02X - %s\n", msg_id, label);
1114 output += body;
1115 output += "\n\n";
1116 }
1117
1118 return output;
1119}
1120
1121// ===========================================================================
1122// Expanded Message Bank
1123// ===========================================================================
1124
1125std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos) {
1126 // Expanded messages occupy one contiguous region. A vanilla [BANK] command
1127 // must not redirect parsing into the vanilla second text bank.
1128 return ReadAllTextData(rom, pos, /*max_pos=*/-1,
1129 /*allow_bank_switch=*/false);
1130}
1131
1132std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos, int end) {
1133 if (end < pos) {
1134 return {};
1135 }
1136 // ReadAllTextData's max_pos is exclusive; expanded-region ends are
1137 // configured as inclusive addresses.
1138 return ReadAllTextData(rom, pos, end + 1, /*allow_bank_switch=*/false);
1139}
1140
1141absl::Status WriteExpandedTextData(Rom* rom, int start, int end,
1142 const std::vector<std::string>& messages) {
1143 if (rom == nullptr || !rom->is_loaded()) {
1144 return absl::InvalidArgumentError("ROM not loaded");
1145 }
1146 if (start < 0 || end < start) {
1147 return absl::InvalidArgumentError("Invalid expanded message region");
1148 }
1149
1150 const int capacity = end - start + 1;
1151 if (capacity <= 0) {
1152 return absl::InvalidArgumentError(
1153 "Expanded message region has no capacity");
1154 }
1155
1156 const auto& data = rom->vector();
1157 if (end >= static_cast<int>(data.size())) {
1158 return absl::OutOfRangeError("Expanded message region out of ROM range");
1159 }
1160
1161 // Serialize into a contiguous buffer, then do a single ROM write for safety
1162 // and determinism (and to honor write fences).
1163 std::vector<uint8_t> blob;
1164 blob.reserve(static_cast<size_t>(capacity));
1165
1166 int used = 0;
1167 for (size_t i = 0; i < messages.size(); ++i) {
1168 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1169 if (!parsed.ok()) {
1170 return absl::InvalidArgumentError(
1171 absl::StrFormat("Expanded message %d is invalid: %s",
1172 static_cast<int>(i), parsed.errors.front()));
1173 }
1174 if (messages[i].find("[BANK]") != std::string::npos) {
1175 return absl::InvalidArgumentError(absl::StrFormat(
1176 "Expanded message %d contains [BANK], which is only valid in the "
1177 "vanilla message stream",
1178 static_cast<int>(i)));
1179 }
1180 auto bytes = std::move(parsed.bytes);
1181 const int needed = static_cast<int>(bytes.size()) + 1; // +0x7F
1182
1183 // Always reserve space for the final 0xFF.
1184 if (used + needed + 1 > capacity) {
1185 return absl::ResourceExhaustedError(absl::StrFormat(
1186 "Expanded message data exceeds bank boundary "
1187 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
1188 static_cast<int>(i), used, needed, capacity, end));
1189 }
1190
1191 blob.insert(blob.end(), bytes.begin(), bytes.end());
1192 blob.push_back(kMessageTerminator);
1193 used += needed;
1194 }
1195
1196 if (used + 1 > capacity) {
1197 return absl::ResourceExhaustedError(
1198 "No space for end-of-region marker (0xFF)");
1199 }
1200 blob.push_back(0xFF);
1201
1202 // ROM safety: this writer must only touch the expanded message region.
1203 // NOTE: `end` is inclusive; convert to half-open for the fence.
1205 const uint32_t fence_start = static_cast<uint32_t>(start);
1206 const uint32_t fence_end =
1207 static_cast<uint32_t>(static_cast<uint64_t>(end) + 1ULL);
1208 RETURN_IF_ERROR(fence.Allow(fence_start, fence_end, "ExpandedMessageBank"));
1209 yaze::rom::ScopedWriteFence scope(rom, &fence);
1210
1211 return rom->WriteVector(start, std::move(blob));
1212}
1213
1214absl::Status WriteExpandedTextData(uint8_t* rom, int start, int end,
1215 const std::vector<std::string>& messages) {
1216 if (rom == nullptr || start < 0 || end < start) {
1217 return absl::InvalidArgumentError("Invalid expanded message region");
1218 }
1219
1220 const int capacity = end - start + 1;
1221 int used = 0;
1222 std::vector<std::vector<uint8_t>> encoded_messages;
1223 encoded_messages.reserve(messages.size());
1224
1225 for (size_t i = 0; i < messages.size(); ++i) {
1226 auto parsed = ParseMessageToDataWithDiagnostics(messages[i]);
1227 if (!parsed.ok()) {
1228 return absl::InvalidArgumentError(
1229 absl::StrFormat("Expanded message %d is invalid: %s",
1230 static_cast<int>(i), parsed.errors.front()));
1231 }
1232 if (messages[i].find("[BANK]") != std::string::npos) {
1233 return absl::InvalidArgumentError(absl::StrFormat(
1234 "Expanded message %d contains [BANK], which is only valid in the "
1235 "vanilla message stream",
1236 static_cast<int>(i)));
1237 }
1238 auto bytes = std::move(parsed.bytes);
1239
1240 const int needed = static_cast<int>(bytes.size()) + 1; // +1 for 0x7F
1241 if (used + needed + 1 > capacity) {
1242 return absl::ResourceExhaustedError(
1243 absl::StrFormat("Expanded message data exceeds bank boundary "
1244 "(at message %d, used %d, end 0x%06X)",
1245 static_cast<int>(i), used, end));
1246 }
1247 used += needed;
1248 encoded_messages.push_back(std::move(bytes));
1249 }
1250
1251 int pos = start;
1252 for (const auto& bytes : encoded_messages) {
1253 for (uint8_t byte : bytes) {
1254 rom[pos++] = byte;
1255 }
1256 rom[pos++] = kMessageTerminator;
1257 }
1258
1259 rom[pos++] = 0xFF;
1260
1261 return absl::OkStatus();
1262}
1263
1264absl::Status WriteAllTextData(Rom* rom,
1265 const std::vector<MessageData>& messages) {
1266 if (rom == nullptr || !rom->is_loaded()) {
1267 return absl::InvalidArgumentError("ROM not loaded");
1268 }
1269
1270 ScopedRomTransaction transaction(*rom);
1271
1272 int pos = kTextData;
1273 bool in_second_bank = false;
1274
1275 for (const auto& message : messages) {
1276 bool next_byte_is_command_argument = false;
1277 for (uint8_t value : message.Data) {
1278 RETURN_IF_ERROR(rom->WriteByte(pos, value));
1279
1280 const bool is_command_argument = next_byte_is_command_argument;
1281 next_byte_is_command_argument = false;
1282 if (!is_command_argument && value == kBankSwitchCommand) {
1283 if (!in_second_bank && pos > kTextDataEnd) {
1284 return absl::ResourceExhaustedError(absl::StrFormat(
1285 "Text data exceeds first bank (pos 0x%06X)", pos));
1286 }
1287 pos = kTextData2 - 1;
1288 in_second_bank = true;
1289 }
1290
1291 if (!is_command_argument) {
1292 const auto command = FindMatchingCommand(value);
1293 next_byte_is_command_argument =
1294 command.has_value() && command->HasArgument;
1295 }
1296
1297 pos++;
1298 }
1299
1301 }
1302
1303 if (!in_second_bank && pos > kTextDataEnd) {
1304 return absl::ResourceExhaustedError(
1305 absl::StrFormat("Text data exceeds first bank (pos 0x%06X)", pos));
1306 }
1307
1308 if (in_second_bank && pos > kTextData2End) {
1309 return absl::ResourceExhaustedError(
1310 absl::StrFormat("Text data exceeds second bank (pos 0x%06X)", pos));
1311 }
1312
1313 RETURN_IF_ERROR(rom->WriteByte(pos, 0xFF));
1314 transaction.Commit();
1315 return absl::OkStatus();
1316}
1317
1318} // namespace editor
1319} // 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:227
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:586
auto mutable_data()
Definition rom.h:152
const auto & vector() const
Definition rom.h:155
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:658
auto data() const
Definition rom.h:151
bool is_loaded() const
Definition rom.h:144
static RomSettings & Get()
uint32_t GetAddressOr(const std::string &key, uint32_t default_value) const
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
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)
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)
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
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