1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
|
#include "MessageViewer.h"
#include "2s2h/BenGui/UIWidgets.hpp"
#include "2s2h/BenGui/BenGui.hpp"
#include "2s2h/CustomMessage/CustomMessage.h"
#include "BenPort.h"
#include <message_data_static.h>
#include <sstream>
extern "C" {
#include "functions.h"
#include "macros.h"
#include "variables.h"
}
using namespace UIWidgets;
static std::string ParseEscapeSequences(const std::string& input) {
std::string output;
for (size_t i = 0; i < input.length(); ++i) {
if (i + 3 < input.length() && input[i] == '\\' && input[i + 1] == 'x') {
char hex[3] = { input[i + 2], input[i + 3], '\0' };
char* endptr;
unsigned char byte = (unsigned char)strtol(hex, &endptr, 16);
if (endptr == hex + 2) { // Successfully parsed 2 hex digits
output += (char)byte;
i += 3; // Skip \xXX
} else {
output += input[i]; // Not valid hex, keep as-is
}
} else {
output += input[i];
}
}
return output;
}
static bool ValidateTextIdExists(uint16_t textId) {
if (gPlayState == nullptr) {
return false;
}
MessageTableEntry* msgEntry = gPlayState->msgCtx.messageTableNES;
if (msgEntry == nullptr) {
return false;
}
while (msgEntry->textId != 0xFFFF) {
if (msgEntry->textId == textId) {
return true;
}
msgEntry++;
}
return false;
}
static void FormatByteAsEscapeSequence(std::ostringstream& stream, uint8_t value) {
stream << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)value;
}
void MessageViewerWindow::InitElement() {
mTextIdBuf = static_cast<char*>(calloc(MAX_STRING_SIZE, sizeof(char)));
mCustomMessageBuf = static_cast<char*>(calloc(MAX_STRING_SIZE, sizeof(char)));
}
MessageViewerWindow::~MessageViewerWindow() {
free(mTextIdBuf);
free(mCustomMessageBuf);
}
void MessageViewerWindow::DrawElement() {
// Vanilla Message Viewer Section
ImGui::SeparatorText("Vanilla Message Viewer");
PushStyleInput(THEME_COLOR);
switch (mTextIdBase) {
case DECIMAL:
ImGui::InputText("##TextID", mTextIdBuf, MAX_STRING_SIZE, ImGuiInputTextFlags_CharsDecimal);
break;
case HEXADECIMAL:
default:
ImGui::InputText("##TextID", mTextIdBuf, MAX_STRING_SIZE, ImGuiInputTextFlags_CharsHexadecimal);
break;
}
// Draw placeholder overlay immediately if empty
if (strlen(mTextIdBuf) == 0) {
ImVec2 inputMin = ImGui::GetItemRectMin();
ImVec2 textPos =
ImVec2(inputMin.x + ImGui::GetStyle().FramePadding.x + 4.0f, inputMin.y + ImGui::GetStyle().FramePadding.y);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddText(textPos, ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 0.4f)), "TextID");
}
PopStyleInput();
// Draw radio buttons on same line as input
ImGui::SameLine();
PushStyleCheckbox(THEME_COLOR);
if (ImGui::RadioButton("Hex", &mTextIdBase, HEXADECIMAL)) {
memset(mTextIdBuf, 0, sizeof(char) * MAX_STRING_SIZE);
}
Tooltip("Hexadecimal Text ID of the message to load. Hexadecimal digits only (0-9/A-F).");
ImGui::SameLine();
if (ImGui::RadioButton("Dec", &mTextIdBase, DECIMAL)) {
memset(mTextIdBuf, 0, sizeof(char) * MAX_STRING_SIZE);
}
Tooltip("Decimal Text ID of the message to load. Decimal digits only (0-9).");
PopStyleCheckbox();
PushStyleButton(THEME_COLOR);
if (ImGui::Button("Display Message##ExistingMessage")) {
mDisplayExistingMessageClicked = true;
}
ImGui::SameLine();
if (ImGui::Button("Load Message##ExistingMessage")) {
mLoadMessageClicked = true;
}
PopStyleButton();
// Custom Message Builder Section
ImGui::SeparatorText("Custom Message Builder");
PushStyleInput(THEME_COLOR);
ImGui::InputTextMultiline("##CustomMessage", mCustomMessageBuf, MAX_STRING_SIZE, ImVec2(-1, 100));
PopStyleInput();
Tooltip(
"Enter text to preview in-game. Supports color codes (%r, %w, %y, %g, %b, %p) and escape sequences (\\xXX).\n"
"Use 'Load Message' to inspect vanilla message format. Newlines are stripped from simple text.");
PushStyleButton(THEME_COLOR);
if (ImGui::Button("Display Message##CustomMessage")) {
mDisplayCustomMessageClicked = true;
}
PopStyleButton();
}
void MessageViewerWindow::UpdateElement() {
if (mDisplayExistingMessageClicked) {
if (ParseTextIdFromBuffer(mTextId)) {
DisplayExistingMessage();
}
mDisplayExistingMessageClicked = false;
}
if (mLoadMessageClicked) {
if (ParseTextIdFromBuffer(mTextId)) {
LoadMessageToEditor();
}
mLoadMessageClicked = false;
}
if (mDisplayCustomMessageClicked) {
mCustomMessageString = std::string(mCustomMessageBuf);
// Check if custom message buffer is empty
if (mCustomMessageString.empty()) {
mDisplayCustomMessageClicked = false;
return;
}
std::erase(mCustomMessageString, '\n');
DisplayCustomMessage();
mDisplayCustomMessageClicked = false;
}
}
void MessageViewerWindow::DisplayExistingMessage() const {
MessageDebug_StartTextBox("", mTextId, LANGUAGE_ENG);
}
void MessageViewerWindow::DisplayCustomMessage() const {
MessageDebug_DisplayCustomMessage(mCustomMessageString.c_str());
}
void MessageViewerWindow::LoadMessageToEditor() {
if (!ValidateTextIdExists(mTextId)) {
return;
}
// Load the vanilla message entry
CustomMessage::Entry entry = CustomMessage::LoadVanillaMessageTableEntry(mTextId);
// Message header format (11 bytes) - see: https://wiki.cloudmodding.com/mm/Text_Format
// Bytes 0-1 are stored as raw bytes in segment[] (not packed)
// Game code reads bytes 0-1 as a 16-bit value and extracts fields using bit masks (see Message_OpenText() in
// z_message.c:3396-3402) Message_DecodeHeader() reads bytes 2-10 (icon, nextTextId, costs, padding) - see
// z_message.c:2131-2173 We store/load raw bytes directly to match CustomMessage system format
std::ostringstream rawMessage;
// Format header bytes - write raw bytes as stored in segment (matching CustomMessage::LoadCustomMessageIntoFont)
FormatByteAsEscapeSequence(rawMessage, entry.textboxType);
FormatByteAsEscapeSequence(rawMessage, entry.textboxYPos);
FormatByteAsEscapeSequence(rawMessage, entry.icon);
FormatByteAsEscapeSequence(rawMessage, (entry.nextMessageID & 0xFF00) >> 8);
FormatByteAsEscapeSequence(rawMessage, entry.nextMessageID & 0x00FF);
FormatByteAsEscapeSequence(rawMessage, (entry.firstItemCost & 0xFF00) >> 8);
FormatByteAsEscapeSequence(rawMessage, entry.firstItemCost & 0x00FF);
FormatByteAsEscapeSequence(rawMessage, (entry.secondItemCost & 0xFF00) >> 8);
FormatByteAsEscapeSequence(rawMessage, entry.secondItemCost & 0x00FF);
rawMessage << "\\xff\\xff"; // Padding bytes
// Format message content bytes
for (size_t i = 0; i < entry.msg.length(); ++i) {
unsigned char byte = entry.msg[i];
if (byte >= 0x20 && byte <= 0x7E) {
// Printable ASCII - add as-is
rawMessage << (char)byte;
} else {
// Control code - format as \xXX
FormatByteAsEscapeSequence(rawMessage, byte);
}
}
// Copy the formatted string to the custom message buffer
std::string formattedMessage = rawMessage.str();
if (formattedMessage.size() >= MAX_STRING_SIZE) {
formattedMessage = formattedMessage.substr(0, MAX_STRING_SIZE - 1);
}
strncpy(mCustomMessageBuf, formattedMessage.c_str(), MAX_STRING_SIZE - 1);
mCustomMessageBuf[MAX_STRING_SIZE - 1] = '\0';
}
bool MessageViewerWindow::ParseTextIdFromBuffer(uint16_t& outTextId) {
if (strlen(mTextIdBuf) == 0) {
return false;
}
try {
switch (mTextIdBase) {
case DECIMAL:
outTextId = std::stoi(std::string(mTextIdBuf), nullptr, 10);
break;
case HEXADECIMAL:
default:
outTextId = std::stoi(std::string(mTextIdBuf), nullptr, 16);
break;
}
return true;
} catch (const std::exception&) { return false; }
}
void MessageDebug_StartTextBox(const char* tableId, uint16_t textId, uint8_t language) {
if (!ValidateTextIdExists(textId)) {
return;
}
const auto player = GET_PLAYER(gPlayState);
if (player == nullptr) {
return;
}
Message_StartTextbox(gPlayState, textId, &player->actor);
}
void MessageDebug_DisplayCustomMessage(const char* customMessage) {
if (gPlayState == nullptr) {
return;
}
const auto player = GET_PLAYER(gPlayState);
if (player == nullptr) {
return;
}
// Parse escape sequences in the input
std::string processedMessage = ParseEscapeSequences(customMessage);
// Create a custom message entry
CustomMessage::Entry entry;
// Check if message starts with header (11+ bytes)
// Header detection: verify processed message has at least MESSAGE_HEADER_SIZE bytes
// and first byte is in control character range (< 0x20), indicating it's likely a header byte
if (processedMessage.length() >= MESSAGE_HEADER_SIZE && (unsigned char)processedMessage[0] < 0x20) {
// Parse header bytes - read raw bytes as stored (matching CustomMessage::LoadVanillaMessageTableEntry)
// Game code unpacks these bytes using bit masks, but we store/load raw bytes
entry.textboxType = processedMessage[0];
entry.textboxYPos = processedMessage[1];
entry.icon = (unsigned char)processedMessage[2];
entry.nextMessageID = ((unsigned char)processedMessage[3] << 8) | (unsigned char)processedMessage[4];
entry.firstItemCost = ((unsigned char)processedMessage[5] << 8) | (unsigned char)processedMessage[6];
entry.secondItemCost = ((unsigned char)processedMessage[7] << 8) | (unsigned char)processedMessage[8];
// Skip header, use remaining as message content
entry.msg = processedMessage.substr(MESSAGE_HEADER_SIZE);
entry.autoFormat = false; // Already formatted
} else {
// No header - use defaults for user-written messages
entry.textboxType = 0;
entry.textboxYPos = 0x30;
entry.icon = 0xFE;
entry.nextMessageID = 0xFFFF;
entry.firstItemCost = 0xFFFF;
entry.secondItemCost = 0xFFFF;
entry.msg = processedMessage;
entry.autoFormat = true;
}
// Set the active custom message and display it
CustomMessage::StartTextbox(entry.msg, entry);
}
|