yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_data.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_MESSAGE_MESSAGE_DATA_H
2#define YAZE_APP_EDITOR_MESSAGE_MESSAGE_DATA_H
3
4// ===========================================================================
5// Message Data System for Zelda 3 (A Link to the Past)
6// ===========================================================================
7//
8// This system handles the parsing, editing, and serialization of in-game text
9// messages from The Legend of Zelda: A Link to the Past (SNES).
10//
11// ## Architecture Overview
12//
13// The message system consists of several key components:
14//
15// 1. **Character Encoding** (`CharEncoder`):
16// Maps byte values (0x00-0x66) to displayable characters (A-Z, a-z, 0-9,
17// punctuation). This is the basic text representation in the ROM.
18//
19// 2. **Text Commands** (`TextCommands`):
20// Special control codes (0x67-0x80) that control message display behavior:
21// - Window appearance (border, position)
22// - Text flow (line breaks, scrolling, delays)
23// - Interactive elements (choices, player name insertion)
24// - Some commands have arguments (e.g., [W:02] = window border type 2)
25//
26// 3. **Special Characters** (`SpecialChars`):
27// Extended character set (0x43-0x5E) for game-specific symbols:
28// - Directional arrows
29// - Button prompts (A, B, X, Y)
30// - HP indicators
31// - Hieroglyphs
32//
33// 4. **Dictionary System** (`DictionaryEntry`):
34// Compression system using byte values 0x88+ to reference common
35// words/phrases stored separately in ROM. This saves space by replacing
36// frequently-used text with single-byte references.
37//
38// 5. **Message Data** (`MessageData`):
39// Represents a single in-game message with both raw binary data and parsed
40// human-readable text. Each message is terminated by 0x7F in ROM.
41//
42// ## Data Flow
43//
44// ### Reading from ROM:
45// ROM bytes → ReadAllTextData() → MessageData (raw) → ParseMessageData() →
46// Human-readable string with [command] tokens
47//
48// ### Writing to ROM:
49// User edits text → ParseMessageToData() → Binary bytes → ROM
50//
51// ### Dictionary Optimization:
52// Text string → OptimizeMessageForDictionary() → Replace common phrases with
53// [D:XX] tokens → Smaller binary representation
54//
55// ## ROM Memory Layout (SNES)
56//
57// - Text Data Block 1: 0xE0000 - 0xE7FFF (32KB)
58// - Text Data Block 2: 0x75F40 - 0x773FF (5.3KB)
59// - Dictionary Pointers: 0x74703
60// - Character Widths: Table storing pixel widths for proportional font
61// - Font Graphics: 0x70000+ (2bpp tile data)
62//
63// ## Message Format
64//
65// Messages are stored as byte sequences terminated by 0x7F:
66// Example: [0x00, 0x01, 0x02, 0x7F] = "ABC"
67// Example: [0x6A, 0x59, 0x2C, 0x61, 0x32, 0x28, 0x2B, 0x23, 0x7F]
68// = "[L] saved Hyrule" (0x6A = player name command)
69//
70// ## Token Syntax (Human-Readable Format)
71//
72// Commands: [TOKEN:HEX] or [TOKEN]
73// Examples: [W:02] (window border), [K] (wait for key)
74// Dictionary: [D:HEX]
75// Examples: [D:00] (first dictionary entry)
76// Special Chars:[TOKEN]
77// Examples: [A] (A button), [UP] (up arrow)
78//
79// ===========================================================================
80
81#include <cstddef>
82#include <cstdint>
83#include <optional>
84#include <regex>
85#include <string>
86#include <string_view>
87#include <unordered_map>
88#include <utility>
89#include <vector>
90
91#include <nlohmann/json.hpp>
92#include "absl/status/status.h"
93#include "absl/status/statusor.h"
94#include "absl/strings/match.h"
95#include "absl/strings/str_format.h"
96#include "absl/strings/str_replace.h"
97#include "rom/rom.h"
98
99namespace yaze {
100namespace editor {
101
102const std::string kBankToken = "BANK";
103const std::string DICTIONARYTOKEN = "D";
104constexpr uint8_t kMessageTerminator = 0x7F; // Marks end of message in ROM
105constexpr uint8_t DICTOFF = 0x88; // Dictionary entries start at byte 0x88
106constexpr uint8_t kWidthArraySize = 100;
107constexpr uint8_t kBankSwitchCommand = 0x80;
108
109// Character encoding table: Maps ROM byte values to displayable characters
110// Used for both parsing ROM data into text and converting text back to bytes
111static const std::unordered_map<uint8_t, wchar_t> CharEncoder = {
112 {0x00, 'A'}, {0x01, 'B'}, {0x02, 'C'}, {0x03, 'D'}, {0x04, 'E'},
113 {0x05, 'F'}, {0x06, 'G'}, {0x07, 'H'}, {0x08, 'I'}, {0x09, 'J'},
114 {0x0A, 'K'}, {0x0B, 'L'}, {0x0C, 'M'}, {0x0D, 'N'}, {0x0E, 'O'},
115 {0x0F, 'P'}, {0x10, 'Q'}, {0x11, 'R'}, {0x12, 'S'}, {0x13, 'T'},
116 {0x14, 'U'}, {0x15, 'V'}, {0x16, 'W'}, {0x17, 'X'}, {0x18, 'Y'},
117 {0x19, 'Z'}, {0x1A, 'a'}, {0x1B, 'b'}, {0x1C, 'c'}, {0x1D, 'd'},
118 {0x1E, 'e'}, {0x1F, 'f'}, {0x20, 'g'}, {0x21, 'h'}, {0x22, 'i'},
119 {0x23, 'j'}, {0x24, 'k'}, {0x25, 'l'}, {0x26, 'm'}, {0x27, 'n'},
120 {0x28, 'o'}, {0x29, 'p'}, {0x2A, 'q'}, {0x2B, 'r'}, {0x2C, 's'},
121 {0x2D, 't'}, {0x2E, 'u'}, {0x2F, 'v'}, {0x30, 'w'}, {0x31, 'x'},
122 {0x32, 'y'}, {0x33, 'z'}, {0x34, '0'}, {0x35, '1'}, {0x36, '2'},
123 {0x37, '3'}, {0x38, '4'}, {0x39, '5'}, {0x3A, '6'}, {0x3B, '7'},
124 {0x3C, '8'}, {0x3D, '9'}, {0x3E, '!'}, {0x3F, '?'}, {0x40, '-'},
125 {0x41, '.'}, {0x42, ','}, {0x44, '>'}, {0x45, '('}, {0x46, ')'},
126 {0x4C, '"'}, {0x51, '\''}, {0x59, ' '}, {0x5A, '<'}, {0x5F, L'¡'},
127 {0x60, L'¡'}, {0x61, L'¡'}, {0x62, L' '}, {0x63, L' '}, {0x64, L' '},
128 {0x65, ' '}, {0x66, '_'},
129};
130
131// Finds the ROM byte value for a given character (reverse lookup in
132// CharEncoder) Returns 0xFF if character is not found
133uint8_t FindMatchingCharacter(char value);
134
135// Checks if a byte value represents a dictionary entry
136// Returns dictionary index (0-96) or -1 if not a dictionary entry
137int8_t FindDictionaryEntry(uint8_t value);
138
139// Converts a human-readable message string (with [command] tokens) into ROM
140// bytes This is the inverse operation of ParseMessageData
141std::vector<uint8_t> ParseMessageToData(std::string str);
142
143// Result of parsing text into message bytes with diagnostics.
145 std::vector<uint8_t> bytes;
146 std::vector<std::string> errors;
147 std::vector<std::string> warnings;
148
149 bool ok() const { return errors.empty(); }
150};
151
152// Converts text into message bytes and captures parse errors/warnings.
153MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str);
154
155// Message bank for bundle import/export.
156enum class MessageBank {
157 kVanilla,
158 kExpanded,
159};
160
161std::string MessageBankToString(MessageBank bank);
162absl::StatusOr<MessageBank> MessageBankFromString(std::string_view value);
163
164// Represents a single dictionary entry (common word/phrase) used for text
165// compression Dictionary entries are stored separately in ROM and referenced by
166// bytes 0x88-0xE8 Example: Dictionary entry 0x00 might contain "the" and be
167// referenced as [D:00]
169 uint8_t ID = 0; // Dictionary index (0-96)
170 std::string Contents = ""; // The actual text this entry represents
171 std::vector<uint8_t> Data; // Binary representation of Contents
172 int Length = 0; // Character count
173 std::string Token = ""; // Human-readable token like "[D:00]"
174
175 DictionaryEntry() = default;
176 DictionaryEntry(uint8_t i, std::string_view s)
177 : ID(i), Contents(s), Length(s.length()) {
178 Token = absl::StrFormat("[%s:%02X]", DICTIONARYTOKEN, ID);
180 }
181
182 // Checks if this dictionary entry's text appears in the given string
183 bool ContainedInString(std::string_view s) const {
184 // Convert to std::string to avoid Debian string_view bug with
185 // absl::StrContains
186 return absl::StrContains(std::string(s), Contents);
187 }
188
189 // Replaces all occurrences of this dictionary entry's text with its token
190 // Example: "the cat" with dictionary[0]="the" becomes "[D:00] cat"
191 std::string ReplaceInstancesOfIn(std::string_view s) const {
192 auto replaced_string = std::string(s);
193 size_t pos = replaced_string.find(Contents);
194 while (pos != std::string::npos) {
195 replaced_string.replace(pos, Contents.length(), Token);
196 pos = replaced_string.find(Contents, pos + Token.length());
197 }
198 return replaced_string;
199 }
200};
201
202constexpr int kTextData = 0xE0000;
203constexpr int kTextDataEnd = 0xE7FFF;
204constexpr int kNumDictionaryEntries = 0x61;
205constexpr int kPointersDictionaries = 0x74703;
206constexpr uint8_t kScrollVertical = 0x73;
207constexpr uint8_t kLine1 = 0x74;
208constexpr uint8_t kLine2 = 0x75;
209constexpr uint8_t kLine3 = 0x76;
210
211// Reads all dictionary entries from ROM and builds the dictionary table
212std::vector<DictionaryEntry> BuildDictionaryEntries(Rom* rom);
213
214// Replaces all dictionary words in a string with their [D:XX] tokens
215// Used for text compression when saving messages back to ROM
216std::string ReplaceAllDictionaryWords(
217 std::string str, const std::vector<DictionaryEntry>& dictionary);
218
219// Finds the next match position for query in text starting at start_pos.
220// Returns std::nullopt when no match is found or query is empty.
221std::optional<size_t> FindTextMatch(std::string_view text,
222 std::string_view query, size_t start_pos,
223 bool case_sensitive, bool match_whole_word);
224
225// Replaces query occurrences in text and returns replacement count.
226// If replace_all is false, only the first matching occurrence at/after
227// start_pos is replaced.
228int ReplaceTextMatches(std::string* text, std::string_view query,
229 std::string_view replacement, size_t start_pos,
230 bool replace_all, bool case_sensitive,
231 bool match_whole_word,
232 size_t* first_replaced_pos = nullptr);
233
234// Looks up a dictionary entry by its ROM byte value
236 uint8_t value, const std::vector<DictionaryEntry>& dictionary);
237
238// Special marker inserted into commands to protect them from dictionary
239// replacements during optimization. Removed after dictionary replacement is
240// complete.
241const std::string CHEESE = "\uBEBE";
242
243// Represents a complete in-game message with both raw and parsed
244// representations Messages can exist in two forms:
245// 1. Raw: Direct ROM bytes with dictionary references as [D:XX] tokens
246// 2. Parsed: Fully expanded with dictionary words replaced by actual text
248 int ID = 0; // Message index in the ROM
249 int Address = 0; // ROM address where this message is stored
250 std::string RawString; // Human-readable with [D:XX] dictionary tokens
251 std::string ContentsParsed; // Fully expanded human-readable text
252 std::vector<uint8_t> Data; // Raw ROM bytes (may contain dict references)
253 std::vector<uint8_t> DataParsed; // Expanded bytes (dict entries expanded)
254
255 MessageData() = default;
256 MessageData(int id, int address, const std::string& rawString,
257 const std::vector<uint8_t>& rawData,
258 const std::string& parsedString,
259 const std::vector<uint8_t>& parsedData)
260 : ID(id),
261 Address(address),
262 RawString(rawString),
263 ContentsParsed(parsedString),
264 Data(rawData),
265 DataParsed(parsedData) {}
266
267 // Copy constructor
268 MessageData(const MessageData& other) {
269 ID = other.ID;
270 Address = other.Address;
271 RawString = other.RawString;
272 Data = other.Data;
273 DataParsed = other.DataParsed;
275 }
276
277 // Optimizes a message by replacing common phrases with dictionary tokens
278 // Inserts CHEESE markers inside commands to prevent dictionary replacement
279 // from corrupting command syntax like [W:02]
280 // Example: "Link saved the day" → "[D:00] saved [D:01] day"
282 std::string_view message_string,
283 const std::vector<DictionaryEntry>& dictionary) {
284 std::stringstream protons;
285 bool command = false;
286 // Insert CHEESE markers inside commands to protect them
287 for (const auto& c : message_string) {
288 if (c == '[') {
289 command = true;
290 } else if (c == ']') {
291 command = false;
292 }
293
294 protons << c;
295 if (command) {
296 protons << CHEESE; // Protect command contents from replacement
297 }
298 }
299
300 std::string protons_string = protons.str();
301 std::string replaced_string =
302 ReplaceAllDictionaryWords(protons_string, dictionary);
303 std::string final_string =
304 absl::StrReplaceAll(replaced_string, {{CHEESE, ""}});
305
306 return final_string;
307 }
308
309 // Updates this message with new text content
310 // Automatically optimizes the message using dictionary compression
311 void SetMessage(const std::string& message,
312 const std::vector<DictionaryEntry>& dictionary) {
313 RawString = message;
314 ContentsParsed = OptimizeMessageForDictionary(message, dictionary);
315 }
316};
317
318// Message bundle entry for JSON import/export.
320 int id = 0;
322 std::string raw;
323 std::string parsed;
324 std::string text;
325};
326
327constexpr int kMessageBundleVersion = 1;
328
329// Represents a text command or special character definition
330// Text commands control message display (line breaks, colors, choices, etc.)
331// Special characters are game-specific symbols (arrows, buttons, HP hearts)
333 uint8_t ID; // ROM byte value for this element
334 std::string Token; // Short token like "W" or "UP"
335 std::string GenericToken; // Display format like "[W:##]" or "[UP]"
336 std::string Pattern; // Regex pattern for parsing
337 std::string StrictPattern; // Strict regex pattern for exact matching
338 std::string Description; // Human-readable description
339 bool HasArgument; // True if command takes a parameter byte
340
341 TextElement() = default;
342 TextElement(uint8_t id, const std::string& token, bool arg,
343 const std::string& description) {
344 ID = id;
345 Token = token;
346 if (arg) {
347 GenericToken = absl::StrFormat("[%s:##]", Token);
348 } else {
349 GenericToken = absl::StrFormat("[%s]", Token);
350 }
351 HasArgument = arg;
352 Description = description;
353 if (arg) {
354 Pattern = absl::StrFormat(
355 "\\[%s(:[0-9A-F]{1,2})\\]",
356 absl::StrReplaceAll(Token, {{"[", "\\["}, {"]", "\\]"}}));
357 } else {
358 Pattern = absl::StrFormat(
359 "\\[%s\\]", absl::StrReplaceAll(Token, {{"[", "\\["}, {"]", "\\]"}}));
360 }
361 StrictPattern = absl::StrFormat("^%s$", Pattern);
362 }
363
364 std::string GetParamToken(uint8_t value = 0) const {
365 if (HasArgument) {
366 return absl::StrFormat("[%s:%02X]", Token, value);
367 } else {
368 return absl::StrFormat("[%s]", Token);
369 }
370 }
371
372 std::smatch MatchMe(const std::string& dfrag) const {
373 std::regex pattern(StrictPattern);
374 std::smatch match;
375 std::regex_match(dfrag, match, pattern);
376 return match;
377 }
378
379 bool Empty() const { return ID == 0; }
380
381 // Comparison operator
382 bool operator==(const TextElement& other) const { return ID == other.ID; }
383};
384
385const static std::string kWindowBorder = "Window border";
386const static std::string kWindowPosition = "Window position";
387const static std::string kScrollSpeed = "Scroll speed";
388const static std::string kTextDrawSpeed = "Text draw speed";
389const static std::string kTextColor = "Text color";
390const static std::string kPlayerName = "Player name";
391const static std::string kLine1Str = "Line 1";
392const static std::string kLine2Str = "Line 2";
393const static std::string kLine3Str = "Line 3";
394const static std::string kWaitForKey = "Wait for key";
395const static std::string kScrollText = "Scroll text";
396const static std::string kDelayX = "Delay X";
397const static std::string kBCDNumber = "BCD number";
398const static std::string kSoundEffect = "Sound effect";
399const static std::string kChoose3 = "Choose 3";
400const static std::string kChoose2High = "Choose 2 high";
401const static std::string kChoose2Low = "Choose 2 low";
402const static std::string kChoose2Indented = "Choose 2 indented";
403const static std::string kChooseItem = "Choose item";
404const static std::string kNextAttractImage = "Next attract image";
405const static std::string kBankMarker = "Bank marker (automatic)";
406const static std::string kCrash = "Crash";
407
408static const std::vector<TextElement> TextCommands = {
409 TextElement(0x6B, "W", true, kWindowBorder),
410 TextElement(0x6D, "P", true, kWindowPosition),
411 TextElement(0x6E, "SPD", true, kScrollSpeed),
412 TextElement(0x7A, "S", true, kTextDrawSpeed),
413 TextElement(0x77, "C", true, kTextColor),
414 TextElement(0x6A, "L", false, kPlayerName),
415 TextElement(0x74, "1", false, kLine1Str),
416 TextElement(0x75, "2", false, kLine2Str),
417 TextElement(0x76, "3", false, kLine3Str),
418 TextElement(0x7E, "K", false, kWaitForKey),
419 TextElement(0x73, "V", false, kScrollText),
420 TextElement(0x78, "WT", true, kDelayX),
421 TextElement(0x6C, "N", true, kBCDNumber),
422 TextElement(0x79, "SFX", true, kSoundEffect),
423 TextElement(0x71, "CH3", false, kChoose3),
424 TextElement(0x72, "CH2", false, kChoose2High),
425 TextElement(0x6F, "CH2L", false, kChoose2Low),
426 TextElement(0x68, "CH2I", false, kChoose2Indented),
427 TextElement(0x69, "CHI", false, kChooseItem),
428 TextElement(0x67, "IMG", false, kNextAttractImage),
429 TextElement(0x80, kBankToken, false, kBankMarker),
430 TextElement(0x70, "NONO", false, kCrash),
431};
432
433// Finds the TextElement definition for a command byte value
434// Returns nullopt if the byte is not a recognized command
435std::optional<TextElement> FindMatchingCommand(uint8_t b);
436
437// Special characters available in Zelda 3 messages
438// These are symbols and game-specific icons that appear in text
439static const std::vector<TextElement> SpecialChars = {
440 TextElement(0x43, "...", false, "Ellipsis …"),
441 TextElement(0x4D, "UP", false, "Arrow ↑"),
442 TextElement(0x4E, "DOWN", false, "Arrow ↓"),
443 TextElement(0x4F, "LEFT", false, "Arrow ←"),
444 TextElement(0x50, "RIGHT", false, "Arrow →"),
445 TextElement(0x5B, "A", false, "Button Ⓐ"),
446 TextElement(0x5C, "B", false, "Button Ⓑ"),
447 TextElement(0x5D, "X", false, "Button ⓧ"),
448 TextElement(0x5E, "Y", false, "Button ⓨ"),
449 TextElement(0x52, "HP1L", false, "1 HP left"),
450 TextElement(0x53, "HP1R", false, "1 HP right"),
451 TextElement(0x54, "HP2L", false, "2 HP left"),
452 TextElement(0x55, "HP3L", false, "3 HP left"),
453 TextElement(0x56, "HP3R", false, "3 HP right"),
454 TextElement(0x57, "HP4L", false, "4 HP left"),
455 TextElement(0x58, "HP4R", false, "4 HP right"),
456 TextElement(0x47, "HY0", false, "Hieroglyph ☥"),
457 TextElement(0x48, "HY1", false, "Hieroglyph 𓈗"),
458 TextElement(0x49, "HY2", false, "Hieroglyph Ƨ"),
459 TextElement(0x4A, "LFL", false, "Link face left"),
460 TextElement(0x4B, "LFR", false, "Link face right"),
461};
462
463// Finds the TextElement definition for a special character byte
464// Returns nullopt if the byte is not a recognized special character
465std::optional<TextElement> FindMatchingSpecial(uint8_t b);
466
467// Result of parsing a text token like "[W:02]"
468// Contains both the command definition and its argument value
470 TextElement Parent; // The command or special character definition
471 uint8_t Value; // Argument value (if command has argument)
472 bool Active = false; // True if parsing was successful
473
474 ParsedElement() = default;
475 ParsedElement(const TextElement& textElement, uint8_t value)
476 : Parent(textElement), Value(value), Active(true) {}
477};
478
479// Parses a token string like "[W:02]" and returns its ParsedElement
480// Returns inactive ParsedElement if token is invalid
481ParsedElement FindMatchingElement(const std::string& str);
482
483// Converts a single ROM byte into its human-readable text representation
484// Handles characters, commands, special chars, and dictionary references
485std::string ParseTextDataByte(uint8_t value);
486
487// Parses a single message from ROM data starting at current_pos
488// Updates current_pos to point after the message terminator
489// Returns error if message is malformed (e.g., missing terminator)
490absl::StatusOr<MessageData> ParseSingleMessage(
491 const std::vector<uint8_t>& rom_data, int* current_pos);
492
493// Converts MessageData objects into human-readable strings with [command]
494// tokens This is the main function for displaying messages in the editor
495// Properly handles commands with arguments to avoid parsing errors
496std::vector<std::string> ParseMessageData(
497 std::vector<MessageData>& message_data,
498 const std::vector<DictionaryEntry>& dictionary_entries);
499
500constexpr int kTextData2 = 0x75F40;
501constexpr int kTextData2End = 0x773FF;
502
503// One exact, half-open ROM write in a vanilla-message save plan.
505 public:
506 VanillaMessageWrite(uint32_t start, std::vector<uint8_t> bytes)
507 : start_(start), bytes_(std::move(bytes)) {}
508
509 uint32_t start() const { return start_; }
510 uint32_t end() const { return start_ + static_cast<uint32_t>(bytes_.size()); }
511 const std::vector<uint8_t>& bytes() const { return bytes_; }
512
513 bool operator==(const VanillaMessageWrite&) const = default;
514
515 private:
516 uint32_t start_ = 0;
517 std::vector<uint8_t> bytes_;
518};
519
520// Immutable serialization result for the split vanilla message stream.
521//
522// Exactly one standalone [BANK] command is required. `bank_switch_count()`
523// excludes byte value 0x80 when used as an argument (for example, [W:80]).
525 public:
530
531 const std::vector<VanillaMessageWrite>& writes() const { return writes_; }
532 size_t message_count() const { return message_count_; }
533 size_t bank_switch_count() const { return bank_switch_count_; }
534
535 std::vector<std::pair<uint32_t, uint32_t>> write_ranges() const;
536
537 bool operator==(const VanillaMessageSavePlan&) const = default;
538
539 private:
540 friend absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
541 const std::vector<MessageData>& messages,
542 std::optional<size_t> expected_message_count);
543
544 VanillaMessageSavePlan(std::vector<VanillaMessageWrite> writes,
545 size_t message_count, size_t bank_switch_count)
546 : writes_(std::move(writes)),
549
550 std::vector<VanillaMessageWrite> writes_;
551 size_t message_count_ = 0;
553};
554
555// Reads all text data from the ROM and returns a vector of MessageData objects.
556// When max_pos > 0, the parser stops if pos exceeds max_pos (safety bound).
557// Set allow_bank_switch=false for contiguous banks such as expanded messages.
558std::vector<MessageData> ReadAllTextData(uint8_t* rom, int pos = kTextData,
559 int max_pos = -1,
560 bool allow_bank_switch = true);
561
562// Calls the file dialog and loads expanded messages from a BIN file.
563absl::Status LoadExpandedMessages(std::string& expanded_message_path,
564 std::vector<std::string>& parsed_messages,
565 std::vector<MessageData>& expanded_messages,
566 std::vector<DictionaryEntry>& dictionary);
567
568// Serializes a vector of MessageData to a JSON object.
569nlohmann::json SerializeMessagesToJson(
570 const std::vector<MessageData>& messages);
571
572// Exports messages to a JSON file at the specified path.
573absl::Status ExportMessagesToJson(const std::string& path,
574 const std::vector<MessageData>& messages);
575
576// Serializes message bundles (vanilla + expanded) to JSON.
577nlohmann::json SerializeMessageBundle(const std::vector<MessageData>& vanilla,
578 const std::vector<MessageData>& expanded);
579
580// Exports message bundle to JSON file.
581absl::Status ExportMessageBundleToJson(
582 const std::string& path, const std::vector<MessageData>& vanilla,
583 const std::vector<MessageData>& expanded);
584
585// Parses message bundle JSON into entries.
586absl::StatusOr<std::vector<MessageBundleEntry>> ParseMessageBundleJson(
587 const nlohmann::json& json);
588
589// Loads message bundle entries from a JSON file.
590absl::StatusOr<std::vector<MessageBundleEntry>> LoadMessageBundleFromJson(
591 const std::string& path);
592
593// ===========================================================================
594// Line Width Validation
595// ===========================================================================
596
597constexpr int kMaxLineWidth = 32; // Maximum visible characters per line
598
599// Validates that no line in a message exceeds kMaxLineWidth visible characters.
600// Splits on line break tokens: [1], [2], [3], [V], [K]
601// Returns a vector of warning strings (empty if all lines are within bounds).
602// Command tokens like [W:02], [SFX:2D] etc. are not counted as visible chars.
603std::vector<std::string> ValidateMessageLineWidths(const std::string& message);
604
605// ===========================================================================
606// Org Format (.org) Import/Export
607// ===========================================================================
608
609// Parses an org-mode header line like "** 0F - Skeleton Guard"
610// Returns {message_id, label} pair, or nullopt if not a valid header.
611std::optional<std::pair<int, std::string>> ParseOrgHeader(
612 const std::string& line);
613
614// Parses the full content of a .org file into message entries.
615// Returns a vector of {message_id, body_text} pairs.
616std::vector<std::pair<int, std::string>> ParseOrgContent(
617 const std::string& content);
618
619// Exports messages to .org format string.
620// messages: vector of {message_id, body_text} pairs
621// labels: parallel vector of human-readable labels for each message
622std::string ExportToOrgFormat(
623 const std::vector<std::pair<int, std::string>>& messages,
624 const std::vector<std::string>& labels);
625
626// ===========================================================================
627// Expanded Message Bank (Oracle of Secrets: $2F8000)
628// ===========================================================================
629
630// PC address of SNES $2F8000 (expanded message region start)
631constexpr int kExpandedTextDataDefault = 0x178000;
632// PC address of SNES $2FFFFF (expanded message region end)
633constexpr int kExpandedTextDataEndDefault = 0x17FFFF;
634
637
638// Reads expanded messages from a ROM buffer at the given PC address.
639// Messages are 0x7F-terminated, region is 0xFF-terminated.
640// Uses the same parsing as ReadAllTextData but for the expanded bank.
641std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos);
642
643// Bounded expanded-message reader. `end` is the inclusive final PC address;
644// parsing stops there even when the 0xFF region terminator is missing.
645std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos, int end);
646
647// Writes encoded messages to the expanded region of a ROM buffer.
648// Each message text is encoded via ParseMessageToData, terminated with 0x7F.
649// The region is terminated with 0xFF.
650// Returns error if total size exceeds (end - start + 1).
651//
652// Prefer the Rom* overload when possible:
653// - updates Rom dirty state
654// - is write-fence aware (ROM safety guardrails)
655absl::Status WriteExpandedTextData(Rom* rom, int start, int end,
656 const std::vector<std::string>& messages);
657
658// Legacy buffer overload. This bypasses Rom write fences and does not mark the
659// ROM dirty. Prefer WriteExpandedTextData(Rom*, ...) for new code.
660absl::Status WriteExpandedTextData(uint8_t* rom, int start, int end,
661 const std::vector<std::string>& messages);
662
663// Builds a complete vanilla-message serialization without mutating a ROM.
664// If expected_message_count is present, a mismatch fails closed.
665absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
666 const std::vector<MessageData>& messages,
667 std::optional<size_t> expected_message_count = std::nullopt);
668
669// Applies an immutable vanilla-message plan atomically through an exact write
670// fence. Callers remain responsible for project/manifest policy preflight
671// against plan.write_ranges().
672absl::Status ApplyVanillaMessageSavePlan(Rom* rom,
673 const VanillaMessageSavePlan& plan);
674
675// Writes all vanilla message data back to the ROM with bank switching.
676// Delegates to BuildVanillaMessageSavePlan and ApplyVanillaMessageSavePlan.
677absl::Status WriteAllTextData(Rom* rom,
678 const std::vector<MessageData>& messages);
679
680} // namespace editor
681} // namespace yaze
682
683#endif // YAZE_APP_EDITOR_MESSAGE_MESSAGE_DATA_H
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
const std::vector< VanillaMessageWrite > & writes() const
bool operator==(const VanillaMessageSavePlan &) const =default
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
VanillaMessageSavePlan(const VanillaMessageSavePlan &)=default
VanillaMessageSavePlan(std::vector< VanillaMessageWrite > writes, size_t message_count, size_t bank_switch_count)
std::vector< VanillaMessageWrite > writes_
friend absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
VanillaMessageSavePlan & operator=(VanillaMessageSavePlan &&)=default
VanillaMessageSavePlan(VanillaMessageSavePlan &&)=default
VanillaMessageSavePlan & operator=(const VanillaMessageSavePlan &)=default
const std::vector< uint8_t > & bytes() const
std::vector< uint8_t > bytes_
bool operator==(const VanillaMessageWrite &) const =default
VanillaMessageWrite(uint32_t start, std::vector< uint8_t > bytes)
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)
const std::string CHEESE
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 uint8_t kLine1
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)
constexpr uint8_t kWidthArraySize
absl::Status ExportMessageBundleToJson(const std::string &path, const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr uint8_t DICTOFF
std::string ExportToOrgFormat(const std::vector< std::pair< int, std::string > > &messages, const std::vector< std::string > &labels)
absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
std::optional< TextElement > FindMatchingCommand(uint8_t b)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
ParsedElement FindMatchingElement(const std::string &str)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
std::vector< std::pair< int, std::string > > ParseOrgContent(const std::string &content)
constexpr int kExpandedTextDataDefault
constexpr uint8_t kLine3
int8_t FindDictionaryEntry(uint8_t value)
absl::Status ApplyVanillaMessageSavePlan(Rom *rom, const VanillaMessageSavePlan &plan)
constexpr int kTextDataEnd
bool ContainedInString(std::string_view s) const
std::string ReplaceInstancesOfIn(std::string_view s) const
DictionaryEntry(uint8_t i, std::string_view s)
std::vector< uint8_t > Data
MessageData(const MessageData &other)
std::vector< uint8_t > Data
std::vector< uint8_t > DataParsed
std::string OptimizeMessageForDictionary(std::string_view message_string, const std::vector< DictionaryEntry > &dictionary)
void SetMessage(const std::string &message, const std::vector< DictionaryEntry > &dictionary)
MessageData(int id, int address, const std::string &rawString, const std::vector< uint8_t > &rawData, const std::string &parsedString, const std::vector< uint8_t > &parsedData)
std::vector< uint8_t > bytes
std::vector< std::string > errors
std::vector< std::string > warnings
ParsedElement(const TextElement &textElement, uint8_t value)
std::smatch MatchMe(const std::string &dfrag) const
TextElement(uint8_t id, const std::string &token, bool arg, const std::string &description)
bool operator==(const TextElement &other) const
std::string GetParamToken(uint8_t value=0) const