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} // namespace
157
159 Rom* rom, const resources::ArgumentParser& args,
160 resources::OutputFormatter& formatter) {
161 bool verbose = args.HasFlag("verbose");
162
163 DiagnosticReport report;
164
165 if (!rom || !rom->is_loaded()) {
166 return absl::InvalidArgumentError("ROM not loaded");
167 }
168
169 // 1. Build Dictionary
170 std::vector<editor::DictionaryEntry> dictionary;
171 try {
172 dictionary = editor::BuildDictionaryEntries(rom);
173 } catch (const std::exception& e) {
174 DiagnosticFinding finding;
175 finding.id = "dictionary_build_failed";
177 finding.message =
178 absl::StrFormat("Failed to build dictionary: %s", e.what());
179 finding.location = "Dictionary Tables";
180 finding.fixable = false;
181 report.AddFinding(finding);
182
183 // Cannot proceed without dictionary
184 formatter.AddField("total_findings", report.TotalFindings());
185 formatter.AddField("critical_count", report.critical_count);
186 return absl::OkStatus();
187 }
188
189 // 2. Scan Messages
190 std::vector<editor::MessageData> messages;
191 try {
192 uint8_t* mutable_data = const_cast<uint8_t*>(rom->data());
193 messages = editor::ReadAllTextData(mutable_data, editor::kTextData);
194
195 } catch (const std::exception& e) {
196 DiagnosticFinding finding;
197 finding.id = "message_scan_failed";
199 finding.message = absl::StrFormat("Failed to scan messages: %s", e.what());
200 finding.location = "Message Data Region";
201 finding.fixable = false;
202 report.AddFinding(finding);
203 }
204
205 // 3. Analyze Each Message
206 int valid_count = 0;
207 int empty_count = 0;
208
209 for (const auto& msg : messages) {
210 if (msg.Data.empty()) {
211 empty_count++;
212 continue;
213 }
214
215 // Run all validations
216 ValidateControlCodes(msg, report);
217 ValidateTerminators(msg, report);
218 ValidateDictionaryRefs(msg, dictionary, report);
219 CheckCorruptionPatterns(msg, report);
220
221 valid_count++;
222 }
223
224 // 4. Output results
225 formatter.AddField("messages_scanned", static_cast<int>(messages.size()));
226 formatter.AddField("valid_messages", valid_count);
227 formatter.AddField("empty_messages", empty_count);
228 formatter.AddField("dictionary_entries", static_cast<int>(dictionary.size()));
229 formatter.AddField("total_findings", report.TotalFindings());
230 formatter.AddField("critical_count", report.critical_count);
231 formatter.AddField("error_count", report.error_count);
232 formatter.AddField("warning_count", report.warning_count);
233 formatter.AddField("info_count", report.info_count);
234 formatter.AddField("fixable_count", report.fixable_count);
235
236 // Output findings array for JSON
237 if (formatter.IsJson()) {
238 formatter.BeginArray("findings");
239 for (const auto& finding : report.findings) {
240 formatter.AddArrayItem(finding.FormatJson());
241 }
242 formatter.EndArray();
243 }
244
245 // Text output
246 if (!formatter.IsJson()) {
247 std::cout << "\n";
248 std::cout << "╔════════════════════════════════════════════════════════════"
249 "═══╗\n";
250 std::cout << "║ MESSAGE DOCTOR "
251 " ║\n";
252 std::cout << "╠════════════════════════════════════════════════════════════"
253 "═══╣\n";
254 std::cout << absl::StrFormat("║ Messages Scanned: %-42d ║\n",
255 static_cast<int>(messages.size()));
256 std::cout << absl::StrFormat("║ Valid Messages: %-44d ║\n", valid_count);
257 std::cout << absl::StrFormat("║ Empty Messages: %-44d ║\n", empty_count);
258 std::cout << absl::StrFormat("║ Dictionary Entries: %-40d ║\n",
259 static_cast<int>(dictionary.size()));
260 std::cout << "╠════════════════════════════════════════════════════════════"
261 "═══╣\n";
262 std::cout << absl::StrFormat(
263 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
264 report.TotalFindings(), report.error_count, report.warning_count,
265 report.info_count, "");
266 if (report.fixable_count > 0) {
267 std::cout << absl::StrFormat("║ Fixable Issues: %-44d ║\n",
268 report.fixable_count);
269 }
270 std::cout << "╚════════════════════════════════════════════════════════════"
271 "═══╝\n";
272
273 if (verbose && !report.findings.empty()) {
274 std::cout << "\n=== Detailed Findings ===\n";
275 for (const auto& finding : report.findings) {
276 std::cout << " " << finding.FormatText() << "\n";
277 }
278 } else if (!verbose && report.HasProblems()) {
279 std::cout << "\nUse --verbose to see detailed findings.\n";
280 }
281
282 if (!report.HasProblems()) {
283 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
284 }
285 std::cout << "\n";
286 }
287
288 return absl::OkStatus();
289}
290
291} // namespace cli
292} // 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:24
auto data() const
Definition rom.h:135
bool is_loaded() const
Definition rom.h:128
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)
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos)
constexpr int kTextData
constexpr uint8_t kMessageTerminator
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t DICTOFF
std::optional< TextElement > FindMatchingCommand(uint8_t b)
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