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 =
29 absl::StrFormat("Unknown control code 0x%02X in message %d", byte,
30 msg.ID);
31 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
32 finding.fixable = false;
33 report.AddFinding(finding);
34 } else if (cmd->HasArgument) {
35 // Check if command argument follows when required
36 if (i + 1 >= msg.Data.size()) {
37 DiagnosticFinding finding;
38 finding.id = "missing_command_arg";
40 finding.message =
41 absl::StrFormat("Control code [%s] missing argument in message %d",
42 cmd->Token, msg.ID);
43 finding.location =
44 absl::StrFormat("Message %d offset %zu", msg.ID, i);
45 finding.fixable = false;
46 report.AddFinding(finding);
47 } else {
48 // Skip the argument byte
49 ++i;
50 }
51 }
52 }
53 }
54}
55
56// Check for missing terminators
58 DiagnosticReport& report) {
59 if (msg.Data.empty()) {
60 DiagnosticFinding finding;
61 finding.id = "empty_message";
63 finding.message = absl::StrFormat("Message %d is empty", msg.ID);
64 finding.location = absl::StrFormat("Message %d", msg.ID);
65 finding.fixable = false;
66 report.AddFinding(finding);
67 return;
68 }
69
70 // Check that message ends with terminator (0x7F)
71 if (msg.Data.back() != editor::kMessageTerminator) {
72 DiagnosticFinding finding;
73 finding.id = "missing_terminator";
75 finding.message = absl::StrFormat(
76 "Message %d missing 0x7F terminator (ends with 0x%02X)", msg.ID,
77 msg.Data.back());
78 finding.location = absl::StrFormat("Message %d", msg.ID);
79 finding.suggested_action = "Add 0x7F terminator to end of message";
80 finding.fixable = true;
81 report.AddFinding(finding);
82 }
83}
84
85// Validate dictionary references
87 const std::vector<editor::DictionaryEntry>& dict,
88 DiagnosticReport& report) {
89 for (size_t i = 0; i < msg.Data.size(); ++i) {
90 uint8_t byte = msg.Data[i];
91 if (byte >= editor::DICTOFF) {
92 int dict_idx = byte - editor::DICTOFF;
93 if (dict_idx >= static_cast<int>(dict.size())) {
94 DiagnosticFinding finding;
95 finding.id = "invalid_dict_ref";
97 finding.message = absl::StrFormat(
98 "Invalid dictionary reference [D:%02X] in message %d (max valid: "
99 "%02X)",
100 dict_idx, msg.ID, static_cast<int>(dict.size()) - 1);
101 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
102 finding.fixable = false;
103 report.AddFinding(finding);
104 }
105 }
106 }
107}
108
109// Check for common corruption patterns
111 DiagnosticReport& report) {
112 // Check for large runs of 0x00 or 0xFF
113 int zero_run = 0;
114 int ff_run = 0;
115 const int kCorruptionThreshold = 10;
116
117 for (uint8_t byte : msg.Data) {
118 if (byte == 0x00) {
119 zero_run++;
120 ff_run = 0;
121 } else if (byte == 0xFF) {
122 ff_run++;
123 zero_run = 0;
124 } else {
125 zero_run = 0;
126 ff_run = 0;
127 }
128
129 if (zero_run >= kCorruptionThreshold) {
130 DiagnosticFinding finding;
131 finding.id = "possible_corruption_zeros";
133 finding.message =
134 absl::StrFormat("Large block of zeros (%d bytes) in message %d",
135 zero_run, msg.ID);
136 finding.location = absl::StrFormat("Message %d", msg.ID);
137 finding.suggested_action = "Check if message data is corrupted";
138 finding.fixable = false;
139 report.AddFinding(finding);
140 break; // Only report once per message
141 }
142
143 if (ff_run >= kCorruptionThreshold) {
144 DiagnosticFinding finding;
145 finding.id = "possible_corruption_ff";
147 finding.message =
148 absl::StrFormat("Large block of 0xFF (%d bytes) in message %d",
149 ff_run, msg.ID);
150 finding.location = absl::StrFormat("Message %d", msg.ID);
151 finding.suggested_action = "Check if message data is corrupted or erased";
152 finding.fixable = false;
153 report.AddFinding(finding);
154 break; // Only report once per message
155 }
156 }
157}
158
159} // namespace
160
162 Rom* rom, const resources::ArgumentParser& args,
163 resources::OutputFormatter& formatter) {
164 bool verbose = args.HasFlag("verbose");
165
166 DiagnosticReport report;
167
168 if (!rom || !rom->is_loaded()) {
169 return absl::InvalidArgumentError("ROM not loaded");
170 }
171
172 // 1. Build Dictionary
173 std::vector<editor::DictionaryEntry> dictionary;
174 try {
175 dictionary = editor::BuildDictionaryEntries(rom);
176 } catch (const std::exception& e) {
177 DiagnosticFinding finding;
178 finding.id = "dictionary_build_failed";
180 finding.message =
181 absl::StrFormat("Failed to build dictionary: %s", e.what());
182 finding.location = "Dictionary Tables";
183 finding.fixable = false;
184 report.AddFinding(finding);
185
186 // Cannot proceed without dictionary
187 formatter.AddField("total_findings", report.TotalFindings());
188 formatter.AddField("critical_count", report.critical_count);
189 return absl::OkStatus();
190 }
191
192 // 2. Scan Messages
193 std::vector<editor::MessageData> messages;
194 try {
195 uint8_t* mutable_data = const_cast<uint8_t*>(rom->data());
196 messages = editor::ReadAllTextData(mutable_data, editor::kTextData);
197
198 } catch (const std::exception& e) {
199 DiagnosticFinding finding;
200 finding.id = "message_scan_failed";
202 finding.message = absl::StrFormat("Failed to scan messages: %s", e.what());
203 finding.location = "Message Data Region";
204 finding.fixable = false;
205 report.AddFinding(finding);
206 }
207
208 // 3. Analyze Each Message
209 int valid_count = 0;
210 int empty_count = 0;
211
212 for (const auto& msg : messages) {
213 if (msg.Data.empty()) {
214 empty_count++;
215 continue;
216 }
217
218 // Run all validations
219 ValidateControlCodes(msg, report);
220 ValidateTerminators(msg, report);
221 ValidateDictionaryRefs(msg, dictionary, report);
222 CheckCorruptionPatterns(msg, report);
223
224 valid_count++;
225 }
226
227 // 4. Output results
228 formatter.AddField("messages_scanned", static_cast<int>(messages.size()));
229 formatter.AddField("valid_messages", valid_count);
230 formatter.AddField("empty_messages", empty_count);
231 formatter.AddField("dictionary_entries", static_cast<int>(dictionary.size()));
232 formatter.AddField("total_findings", report.TotalFindings());
233 formatter.AddField("critical_count", report.critical_count);
234 formatter.AddField("error_count", report.error_count);
235 formatter.AddField("warning_count", report.warning_count);
236 formatter.AddField("info_count", report.info_count);
237 formatter.AddField("fixable_count", report.fixable_count);
238
239 // Output findings array for JSON
240 if (formatter.IsJson()) {
241 formatter.BeginArray("findings");
242 for (const auto& finding : report.findings) {
243 formatter.AddArrayItem(finding.FormatJson());
244 }
245 formatter.EndArray();
246 }
247
248 // Text output
249 if (!formatter.IsJson()) {
250 std::cout << "\n";
251 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
252 std::cout << "║ MESSAGE DOCTOR ║\n";
253 std::cout << "╠═══════════════════════════════════════════════════════════════╣\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 << "╠═══════════════════════════════════════════════════════════════╣\n";
261 std::cout << absl::StrFormat(
262 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
263 report.TotalFindings(), report.error_count, report.warning_count,
264 report.info_count, "");
265 if (report.fixable_count > 0) {
266 std::cout << absl::StrFormat("║ Fixable Issues: %-44d ║\n",
267 report.fixable_count);
268 }
269 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
270
271 if (verbose && !report.findings.empty()) {
272 std::cout << "\n=== Detailed Findings ===\n";
273 for (const auto& finding : report.findings) {
274 std::cout << " " << finding.FormatText() << "\n";
275 }
276 } else if (!verbose && report.HasProblems()) {
277 std::cout << "\nUse --verbose to see detailed findings.\n";
278 }
279
280 if (!report.HasProblems()) {
281 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
282 }
283 std::cout << "\n";
284 }
285
286 return absl::OkStatus();
287}
288
289} // namespace cli
290} // 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