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// Raw bytes of each word-dictionary entry, indexed by [D:xx] id, read from the
504// WordDictionary pointer table ($0E:C703). Entry i runs from pointer i to
505// pointer i + 1, as RenderText_LookupWord reads it. Returns an empty vector if
506// the table or an entry lies outside `rom_size`.
507std::vector<std::vector<uint8_t>> ReadDictionaryEntryBytes(const uint8_t* rom,
508 size_t rom_size);
509
510// Re-encodes the plain character runs of an encoded message with the fewest
511// bytes the word dictionary allows (tokens 0x88-0xE8). Character bytes are
512// those below 0x67. Command bytes, their argument bytes, and existing
513// dictionary tokens are copied unchanged, so the game's decoder
514// (RenderText_ParseMessage) draws the same characters.
515std::vector<uint8_t> CompressMessageWithDictionary(
516 const std::vector<uint8_t>& data,
517 const std::vector<std::vector<uint8_t>>& dictionary);
518
519// Replaces dictionary tokens (0x88-0xE8) in an encoded message with their
520// entry bytes, giving the characters the game draws. Command bytes and their
521// arguments are copied unchanged. Tokens past the dictionary are kept.
522std::vector<uint8_t> ExpandMessageDictionary(
523 const std::vector<uint8_t>& data,
524 const std::vector<std::vector<uint8_t>>& dictionary);
525
526// One exact, half-open ROM write in a vanilla-message save plan.
528 public:
529 VanillaMessageWrite(uint32_t start, std::vector<uint8_t> bytes)
530 : start_(start), bytes_(std::move(bytes)) {}
531
532 uint32_t start() const { return start_; }
533 uint32_t end() const { return start_ + static_cast<uint32_t>(bytes_.size()); }
534 const std::vector<uint8_t>& bytes() const { return bytes_; }
535
536 bool operator==(const VanillaMessageWrite&) const = default;
537
538 private:
539 uint32_t start_ = 0;
540 std::vector<uint8_t> bytes_;
541};
542
543// Immutable serialization result for the split vanilla message stream.
544//
545// Exactly one standalone [BANK] command is required. `bank_switch_count()`
546// excludes byte value 0x80 when used as an argument (for example, [W:80]).
548 public:
553
554 const std::vector<VanillaMessageWrite>& writes() const { return writes_; }
555 size_t message_count() const { return message_count_; }
556 size_t bank_switch_count() const { return bank_switch_count_; }
557
558 std::vector<std::pair<uint32_t, uint32_t>> write_ranges() const;
559
560 // Half-open ranges where the plan's bytes differ from `rom`. Bytes the plan
561 // rewrites with their current value are left out, so policy checks see only
562 // the bytes a save actually changes. Bytes past `rom_size` count as changed.
563 std::vector<std::pair<uint32_t, uint32_t>> ChangedRanges(
564 const uint8_t* rom, size_t rom_size) const;
565
566 bool operator==(const VanillaMessageSavePlan&) const = default;
567
568 private:
569 friend absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
570 const std::vector<MessageData>& messages,
571 std::optional<size_t> expected_message_count);
572
573 VanillaMessageSavePlan(std::vector<VanillaMessageWrite> writes,
574 size_t message_count, size_t bank_switch_count)
575 : writes_(std::move(writes)),
578
579 std::vector<VanillaMessageWrite> writes_;
580 size_t message_count_ = 0;
582};
583
584// Reads all text data from the ROM and returns a vector of MessageData objects.
585// When max_pos > 0, the parser stops if pos exceeds max_pos (safety bound).
586// Set allow_bank_switch=false for contiguous banks such as expanded messages.
587std::vector<MessageData> ReadAllTextData(uint8_t* rom, int pos = kTextData,
588 int max_pos = -1,
589 bool allow_bank_switch = true);
590
591// Calls the file dialog and loads expanded messages from a BIN file.
592absl::Status LoadExpandedMessages(std::string& expanded_message_path,
593 std::vector<std::string>& parsed_messages,
594 std::vector<MessageData>& expanded_messages,
595 std::vector<DictionaryEntry>& dictionary);
596
597// Serializes a vector of MessageData to a JSON object.
598nlohmann::json SerializeMessagesToJson(
599 const std::vector<MessageData>& messages);
600
601// Exports messages to a JSON file at the specified path.
602absl::Status ExportMessagesToJson(const std::string& path,
603 const std::vector<MessageData>& messages);
604
605// Serializes message bundles (vanilla + expanded) to JSON.
606nlohmann::json SerializeMessageBundle(const std::vector<MessageData>& vanilla,
607 const std::vector<MessageData>& expanded);
608
609// Exports message bundle to JSON file.
610absl::Status ExportMessageBundleToJson(
611 const std::string& path, const std::vector<MessageData>& vanilla,
612 const std::vector<MessageData>& expanded);
613
614// Parses message bundle JSON into entries.
615absl::StatusOr<std::vector<MessageBundleEntry>> ParseMessageBundleJson(
616 const nlohmann::json& json);
617
618// Loads message bundle entries from a JSON file.
619absl::StatusOr<std::vector<MessageBundleEntry>> LoadMessageBundleFromJson(
620 const std::string& path);
621
622// ===========================================================================
623// Line Width Validation
624// ===========================================================================
625
626constexpr int kMaxLineWidth = 32; // Maximum visible characters per line
627
628// Validates that no line in a message exceeds kMaxLineWidth visible characters.
629// Splits on line break tokens: [1], [2], [3], [V], [K]
630// Returns a vector of warning strings (empty if all lines are within bounds).
631// Command tokens like [W:02], [SFX:2D] etc. are not counted as visible chars.
632std::vector<std::string> ValidateMessageLineWidths(const std::string& message);
633
634// ===========================================================================
635// Org Format (.org) Import/Export
636// ===========================================================================
637
638// Parses an org-mode header line like "** 0F - Skeleton Guard"
639// Returns {message_id, label} pair, or nullopt if not a valid header.
640std::optional<std::pair<int, std::string>> ParseOrgHeader(
641 const std::string& line);
642
643// Parses the full content of a .org file into message entries.
644// Returns a vector of {message_id, body_text} pairs.
645std::vector<std::pair<int, std::string>> ParseOrgContent(
646 const std::string& content);
647
648// Exports messages to .org format string.
649// messages: vector of {message_id, body_text} pairs
650// labels: parallel vector of human-readable labels for each message
651std::string ExportToOrgFormat(
652 const std::vector<std::pair<int, std::string>>& messages,
653 const std::vector<std::string>& labels);
654
655// ===========================================================================
656// Expanded Message Bank (Oracle of Secrets: $2F8000)
657// ===========================================================================
658
659// PC address of SNES $2F8000 (expanded message region start)
660constexpr int kExpandedTextDataDefault = 0x178000;
661// PC address of SNES $2FFFFF (expanded message region end)
662constexpr int kExpandedTextDataEndDefault = 0x17FFFF;
663
666
667// Reads expanded messages from a ROM buffer at the given PC address.
668// Messages are 0x7F-terminated, region is 0xFF-terminated.
669// Uses the same parsing as ReadAllTextData but for the expanded bank.
670std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos);
671
672// Bounded expanded-message reader. `end` is the inclusive final PC address;
673// parsing stops there even when the 0xFF region terminator is missing.
674std::vector<MessageData> ReadExpandedTextData(uint8_t* rom, int pos, int end);
675
676// Writes encoded messages to the expanded region of a ROM buffer.
677// Each message text is encoded via ParseMessageToData, terminated with 0x7F.
678// The region is terminated with 0xFF.
679// Returns error if total size exceeds (end - start + 1).
680//
681// Prefer the Rom* overload when possible:
682// - updates Rom dirty state
683// - is write-fence aware (ROM safety guardrails)
684absl::Status WriteExpandedTextData(Rom* rom, int start, int end,
685 const std::vector<std::string>& messages);
686
687// Legacy buffer overload. This bypasses Rom write fences and does not mark the
688// ROM dirty. Prefer WriteExpandedTextData(Rom*, ...) for new code.
689absl::Status WriteExpandedTextData(uint8_t* rom, int start, int end,
690 const std::vector<std::string>& messages);
691
692// Builds a complete vanilla-message serialization without mutating a ROM.
693// If expected_message_count is present, a mismatch fails closed.
694absl::StatusOr<VanillaMessageSavePlan> BuildVanillaMessageSavePlan(
695 const std::vector<MessageData>& messages,
696 std::optional<size_t> expected_message_count = std::nullopt);
697
698// Applies an immutable vanilla-message plan atomically through an exact write
699// fence. Callers remain responsible for project/manifest policy preflight
700// against plan.write_ranges().
701absl::Status ApplyVanillaMessageSavePlan(Rom* rom,
702 const VanillaMessageSavePlan& plan);
703
704// Writes all vanilla message data back to the ROM with bank switching.
705// Delegates to BuildVanillaMessageSavePlan and ApplyVanillaMessageSavePlan.
706absl::Status WriteAllTextData(Rom* rom,
707 const std::vector<MessageData>& messages);
708
709} // namespace editor
710} // namespace yaze
711
712#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:49
const std::vector< VanillaMessageWrite > & writes() const
std::vector< std::pair< uint32_t, uint32_t > > ChangedRanges(const uint8_t *rom, size_t rom_size) 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
std::vector< uint8_t > CompressMessageWithDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &dictionary)
constexpr int kTextData
std::optional< std::pair< int, std::string > > ParseOrgHeader(const std::string &line)
std::string MessageBankToString(MessageBank bank)
constexpr int kExpandedTextDataEndDefault
constexpr int kTextData2
std::string ReplaceAllDictionaryWords(std::string str, const std::vector< DictionaryEntry > &dictionary)
absl::Status WriteExpandedTextData(Rom *rom, int start, int end, const std::vector< std::string > &messages)
nlohmann::json SerializeMessageBundle(const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr uint8_t kLine2
constexpr int kPointersDictionaries
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
constexpr int kNumDictionaryEntries
absl::StatusOr< MessageData > ParseSingleMessage(const std::vector< uint8_t > &rom_data, int *current_pos)
absl::StatusOr< std::vector< MessageBundleEntry > > ParseMessageBundleJson(const nlohmann::json &json)
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos, bool allow_bank_switch)
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
std::optional< TextElement > FindMatchingSpecial(uint8_t value)
constexpr uint8_t kMessageTerminator
constexpr int kTextData2End
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t kBankSwitchCommand
int ReplaceTextMatches(std::string *text, std::string_view query, std::string_view replacement, size_t start_pos, bool replace_all, bool case_sensitive, bool match_whole_word, size_t *first_replaced_pos)
std::optional< size_t > FindTextMatch(std::string_view text, std::string_view query, size_t start_pos, bool case_sensitive, bool match_whole_word)
std::vector< uint8_t > ParseMessageToData(std::string str)
absl::Status ExportMessagesToJson(const std::string &path, const std::vector< MessageData > &messages)
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::vector< uint8_t > ExpandMessageDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &dictionary)
std::optional< TextElement > FindMatchingCommand(uint8_t b)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
ParsedElement FindMatchingElement(const std::string &str)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
std::vector< std::vector< uint8_t > > ReadDictionaryEntryBytes(const uint8_t *rom, size_t rom_size)
std::vector< std::pair< int, std::string > > ParseOrgContent(const std::string &content)
constexpr int kExpandedTextDataDefault
constexpr uint8_t kLine3
int8_t FindDictionaryEntry(uint8_t value)
absl::Status ApplyVanillaMessageSavePlan(Rom *rom, const VanillaMessageSavePlan &plan)
constexpr int kTextDataEnd
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