yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_doctor_commands.cc
Go to the documentation of this file.
2
3#include <iostream>
4
5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
9#include "rom/rom.h"
10
11namespace yaze {
12namespace cli {
13
14namespace {
15
16// Validate control codes in message data
18 DiagnosticReport& report) {
19 for (size_t i = 0; i < msg.Data.size(); ++i) {
20 uint8_t byte = msg.Data[i];
21 // Control codes are in range 0x67-0x80
22 if (byte >= 0x67 && byte <= 0x80) {
23 auto cmd = editor::FindMatchingCommand(byte);
24 if (!cmd.has_value()) {
25 DiagnosticFinding finding;
26 finding.id = "unknown_control_code";
28 finding.message = absl::StrFormat(
29 "Unknown control code 0x%02X in message %d", byte, msg.ID);
30 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
31 finding.fixable = false;
32 report.AddFinding(finding);
33 } else if (cmd->HasArgument) {
34 // Check if command argument follows when required
35 if (i + 1 >= msg.Data.size()) {
36 DiagnosticFinding finding;
37 finding.id = "missing_command_arg";
39 finding.message = absl::StrFormat(
40 "Control code [%s] missing argument in message %d", cmd->Token,
41 msg.ID);
42 finding.location =
43 absl::StrFormat("Message %d offset %zu", msg.ID, i);
44 finding.fixable = false;
45 report.AddFinding(finding);
46 } else {
47 // Skip the argument byte
48 ++i;
49 }
50 }
51 }
52 }
53}
54
55// Check for missing terminators
57 DiagnosticReport& report) {
58 if (msg.Data.empty()) {
59 DiagnosticFinding finding;
60 finding.id = "empty_message";
62 finding.message = absl::StrFormat("Message %d is empty", msg.ID);
63 finding.location = absl::StrFormat("Message %d", msg.ID);
64 finding.fixable = false;
65 report.AddFinding(finding);
66 return;
67 }
68
69 // Check that message ends with terminator (0x7F)
70 if (msg.Data.back() != editor::kMessageTerminator) {
71 DiagnosticFinding finding;
72 finding.id = "missing_terminator";
74 finding.message =
75 absl::StrFormat("Message %d missing 0x7F terminator (ends with 0x%02X)",
76 msg.ID, msg.Data.back());
77 finding.location = absl::StrFormat("Message %d", msg.ID);
78 finding.suggested_action = "Add 0x7F terminator to end of message";
79 finding.fixable = true;
80 report.AddFinding(finding);
81 }
82}
83
84// Validate dictionary references
86 const std::vector<editor::DictionaryEntry>& dict,
87 DiagnosticReport& report) {
88 for (size_t i = 0; i < msg.Data.size(); ++i) {
89 uint8_t byte = msg.Data[i];
90 if (byte >= editor::DICTOFF) {
91 int dict_idx = byte - editor::DICTOFF;
92 if (dict_idx >= static_cast<int>(dict.size())) {
93 DiagnosticFinding finding;
94 finding.id = "invalid_dict_ref";
96 finding.message = absl::StrFormat(
97 "Invalid dictionary reference [D:%02X] in message %d (max valid: "
98 "%02X)",
99 dict_idx, msg.ID, static_cast<int>(dict.size()) - 1);
100 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
101 finding.fixable = false;
102 report.AddFinding(finding);
103 }
104 }
105 }
106}
107
108// Check for common corruption patterns
110 DiagnosticReport& report) {
111 // Check for large runs of 0x00 or 0xFF
112 int zero_run = 0;
113 int ff_run = 0;
114 const int kCorruptionThreshold = 10;
115
116 for (uint8_t byte : msg.Data) {
117 if (byte == 0x00) {
118 zero_run++;
119 ff_run = 0;
120 } else if (byte == 0xFF) {
121 ff_run++;
122 zero_run = 0;
123 } else {
124 zero_run = 0;
125 ff_run = 0;
126 }
127
128 if (zero_run >= kCorruptionThreshold) {
129 DiagnosticFinding finding;
130 finding.id = "possible_corruption_zeros";
132 finding.message = absl::StrFormat(
133 "Large block of zeros (%d bytes) in message %d", zero_run, msg.ID);
134 finding.location = absl::StrFormat("Message %d", msg.ID);
135 finding.suggested_action = "Check if message data is corrupted";
136 finding.fixable = false;
137 report.AddFinding(finding);
138 break; // Only report once per message
139 }
140
141 if (ff_run >= kCorruptionThreshold) {
142 DiagnosticFinding finding;
143 finding.id = "possible_corruption_ff";
145 finding.message = absl::StrFormat(
146 "Large block of 0xFF (%d bytes) in message %d", ff_run, msg.ID);
147 finding.location = absl::StrFormat("Message %d", msg.ID);
148 finding.suggested_action = "Check if message data is corrupted or erased";
149 finding.fixable = false;
150 report.AddFinding(finding);
151 break; // Only report once per message
152 }
153 }
154}
155
156// Validate line widths in messages
158 DiagnosticReport& report) {
160 for (const auto& warning : warnings) {
161 DiagnosticFinding finding;
162 finding.id = "line_too_wide";
164 finding.message =
165 absl::StrFormat("Message %d: %s", msg.ID, warning);
166 finding.location = absl::StrFormat("Message %d", msg.ID);
167 finding.suggested_action = "Shorten line or add a line break command";
168 finding.fixable = true;
169 report.AddFinding(finding);
170 }
171}
172
173} // namespace
174
176 Rom* rom, const resources::ArgumentParser& args,
177 resources::OutputFormatter& formatter) {
178 bool verbose = args.HasFlag("verbose");
179
180 DiagnosticReport report;
181
182 if (!rom || !rom->is_loaded()) {
183 return absl::InvalidArgumentError("ROM not loaded");
184 }
185
186 // 1. Build Dictionary
187 std::vector<editor::DictionaryEntry> dictionary;
188 try {
189 dictionary = editor::BuildDictionaryEntries(rom);
190 } catch (const std::exception& e) {
191 DiagnosticFinding finding;
192 finding.id = "dictionary_build_failed";
194 finding.message =
195 absl::StrFormat("Failed to build dictionary: %s", e.what());
196 finding.location = "Dictionary Tables";
197 finding.fixable = false;
198 report.AddFinding(finding);
199
200 // Cannot proceed without dictionary
201 formatter.AddField("total_findings", report.TotalFindings());
202 formatter.AddField("critical_count", report.critical_count);
203 return absl::OkStatus();
204 }
205
206 // 2. Scan Messages
207 std::vector<editor::MessageData> messages;
208 try {
209 uint8_t* mutable_data = const_cast<uint8_t*>(rom->data());
210 messages = editor::ReadAllTextData(mutable_data, editor::kTextData,
212
213 } catch (const std::exception& e) {
214 DiagnosticFinding finding;
215 finding.id = "message_scan_failed";
217 finding.message = absl::StrFormat("Failed to scan messages: %s", e.what());
218 finding.location = "Message Data Region";
219 finding.fixable = false;
220 report.AddFinding(finding);
221 }
222
223 // 3. Analyze Each Message
224 int valid_count = 0;
225 int empty_count = 0;
226
227 for (const auto& msg : messages) {
228 if (msg.Data.empty()) {
229 empty_count++;
230 continue;
231 }
232
233 // Run all validations
234 // Note: ValidateTerminators is skipped because ReadAllTextData strips
235 // the 0x7F terminator from stored Data (it's used as the delimiter).
236 // Messages returned by the parser are guaranteed to be properly terminated.
237 ValidateControlCodes(msg, report);
238 ValidateDictionaryRefs(msg, dictionary, report);
239 CheckCorruptionPatterns(msg, report);
240 ValidateLineWidths(msg, report);
241
242 valid_count++;
243 }
244
245 // 4. Output results
246 formatter.AddField("messages_scanned", static_cast<int>(messages.size()));
247 formatter.AddField("valid_messages", valid_count);
248 formatter.AddField("empty_messages", empty_count);
249 formatter.AddField("dictionary_entries", static_cast<int>(dictionary.size()));
250 formatter.AddField("total_findings", report.TotalFindings());
251 formatter.AddField("critical_count", report.critical_count);
252 formatter.AddField("error_count", report.error_count);
253 formatter.AddField("warning_count", report.warning_count);
254 formatter.AddField("info_count", report.info_count);
255 formatter.AddField("fixable_count", report.fixable_count);
256
257 // Output findings array for JSON
258 if (formatter.IsJson()) {
259 formatter.BeginArray("findings");
260 for (const auto& finding : report.findings) {
261 formatter.AddArrayItem(finding.FormatJson());
262 }
263 formatter.EndArray();
264 }
265
266 // Text output
267 if (!formatter.IsJson()) {
268 std::cout << "\n";
269 std::cout << "╔════════════════════════════════════════════════════════════"
270 "═══╗\n";
271 std::cout << "║ MESSAGE DOCTOR "
272 " ║\n";
273 std::cout << "╠════════════════════════════════════════════════════════════"
274 "═══╣\n";
275 std::cout << absl::StrFormat("║ Messages Scanned: %-42d ║\n",
276 static_cast<int>(messages.size()));
277 std::cout << absl::StrFormat("║ Valid Messages: %-44d ║\n", valid_count);
278 std::cout << absl::StrFormat("║ Empty Messages: %-44d ║\n", empty_count);
279 std::cout << absl::StrFormat("║ Dictionary Entries: %-40d ║\n",
280 static_cast<int>(dictionary.size()));
281 std::cout << "╠════════════════════════════════════════════════════════════"
282 "═══╣\n";
283 std::cout << absl::StrFormat(
284 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
285 report.TotalFindings(), report.error_count, report.warning_count,
286 report.info_count, "");
287 if (report.fixable_count > 0) {
288 std::cout << absl::StrFormat("║ Fixable Issues: %-44d ║\n",
289 report.fixable_count);
290 }
291 std::cout << "╚════════════════════════════════════════════════════════════"
292 "═══╝\n";
293
294 if (verbose && !report.findings.empty()) {
295 std::cout << "\n=== Detailed Findings ===\n";
296 for (const auto& finding : report.findings) {
297 std::cout << " " << finding.FormatText() << "\n";
298 }
299 } else if (!verbose && report.HasProblems()) {
300 std::cout << "\nUse --verbose to see detailed findings.\n";
301 }
302
303 if (!report.HasProblems()) {
304 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
305 }
306 std::cout << "\n";
307 }
308
309 return absl::OkStatus();
310}
311
312} // namespace cli
313} // 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
auto data() const
Definition rom.h:139
bool is_loaded() const
Definition rom.h:132
absl::Status Execute(Rom *rom, const resources::ArgumentParser &args, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
bool HasFlag(const std::string &name) const
Check if a flag is present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
void ValidateDictionaryRefs(const editor::MessageData &msg, const std::vector< editor::DictionaryEntry > &dict, DiagnosticReport &report)
void CheckCorruptionPatterns(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateControlCodes(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateTerminators(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateLineWidths(const editor::MessageData &msg, DiagnosticReport &report)
constexpr int kTextData
constexpr uint8_t kMessageTerminator
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos)
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t DICTOFF
std::optional< TextElement > FindMatchingCommand(uint8_t b)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
constexpr int kTextDataEnd
A single diagnostic finding.
Complete diagnostic report.
std::vector< DiagnosticFinding > findings
int TotalFindings() const
Get total finding count.
bool HasProblems() const
Check if report has any critical or error findings.
void AddFinding(const DiagnosticFinding &finding)
Add a finding and update counts.
std::vector< uint8_t > Data