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