yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_menu_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/ascii.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_split.h"
13#include "cli/util/hex_util.h"
15#include "nlohmann/json.hpp"
16#include "rom/rom.h"
17#include "util/macro.h"
20
21namespace yaze::cli::handlers {
22
23namespace {
24
25std::filesystem::path ResolveProjectPath(
26 const resources::ArgumentParser& parser) {
27 if (auto project_opt = parser.GetString("project"); project_opt.has_value()) {
28 return std::filesystem::path(*project_opt);
29 }
30 return std::filesystem::current_path();
31}
32
33std::string FormatSize(uintmax_t size_bytes) {
34 return absl::StrFormat("%llu", static_cast<unsigned long long>(size_bytes));
35}
36
38 return severity == core::OracleMenuValidationSeverity::kError ? "error"
39 : "warning";
40}
41
43 if (!issue.asm_path.empty() && issue.line > 0) {
44 return absl::StrFormat("[%s] %s: %s (%s:%d)",
45 SeverityToString(issue.severity), issue.code,
46 issue.message, issue.asm_path, issue.line);
47 }
48 if (!issue.asm_path.empty()) {
49 return absl::StrFormat("[%s] %s: %s (%s)",
50 SeverityToString(issue.severity), issue.code,
51 issue.message, issue.asm_path);
52 }
53 return absl::StrFormat("[%s] %s: %s", SeverityToString(issue.severity),
54 issue.code, issue.message);
55}
56
57} // namespace
58
60 const resources::ArgumentParser& parser) {
61 if (auto table_filter = parser.GetString("table");
62 table_filter.has_value() && table_filter->empty()) {
63 return absl::InvalidArgumentError("--table cannot be empty");
64 }
65 return absl::OkStatus();
66}
67
69 Rom* /*rom*/, const resources::ArgumentParser& parser,
70 resources::OutputFormatter& formatter) {
71 ASSIGN_OR_RETURN(const auto root,
72 core::ResolveOracleProjectRoot(ResolveProjectPath(parser)));
74
75 const std::string table_filter = parser.GetString("table").value_or("");
76 std::string draw_filter = parser.GetString("draw-filter").value_or("");
77 draw_filter = absl::AsciiStrToLower(draw_filter);
78 const bool missing_bins_only = parser.HasFlag("missing-bins");
79
80 std::vector<const core::OracleMenuBinEntry*> bins;
81 bins.reserve(registry.bins.size());
82 for (const auto& entry : registry.bins) {
83 if (missing_bins_only && entry.exists) {
84 continue;
85 }
86 bins.push_back(&entry);
87 }
88
89 std::vector<const core::OracleMenuDrawRoutine*> draw_routines;
90 draw_routines.reserve(registry.draw_routines.size());
91 for (const auto& routine : registry.draw_routines) {
92 if (!draw_filter.empty()) {
93 std::string label_lower = absl::AsciiStrToLower(routine.label);
94 if (label_lower.find(draw_filter) == std::string::npos) {
95 continue;
96 }
97 }
98 draw_routines.push_back(&routine);
99 }
100
101 std::vector<const core::OracleMenuComponent*> components;
102 components.reserve(registry.components.size());
103 for (const auto& component : registry.components) {
104 if (!table_filter.empty() && component.table_label != table_filter) {
105 continue;
106 }
107 components.push_back(&component);
108 }
109
110 formatter.AddField("project_root", root.string());
111 formatter.AddField("asm_files", static_cast<int>(registry.asm_files.size()));
112 formatter.AddField("bin_count", static_cast<int>(bins.size()));
113 formatter.AddField("draw_routine_count",
114 static_cast<int>(draw_routines.size()));
115 formatter.AddField("component_count", static_cast<int>(components.size()));
116 formatter.AddField("warnings", static_cast<int>(registry.warnings.size()));
117
118 formatter.BeginArray("bins");
119 for (const auto* entry : bins) {
120 formatter.AddArrayItem(absl::StrFormat(
121 "%s | %s:%d | %s | %s bytes | %s",
122 entry->label.empty() ? "(unlabeled)" : entry->label, entry->asm_path,
123 entry->line, entry->resolved_bin_path, FormatSize(entry->size_bytes),
124 entry->exists ? "ok" : "missing"));
125 }
126 formatter.EndArray();
127
128 formatter.BeginArray("draw_routines");
129 for (const auto* routine : draw_routines) {
130 formatter.AddArrayItem(absl::StrFormat(
131 "%s | %s:%d | refs=%d%s", routine->label, routine->asm_path,
132 routine->line, routine->references, routine->local ? " | local" : ""));
133 }
134 formatter.EndArray();
135
136 formatter.BeginArray("components");
137 for (const auto* component : components) {
138 formatter.AddArrayItem(
139 absl::StrFormat("%s[%d] | (%d,%d) | %s:%d%s%s", component->table_label,
140 component->index, component->row, component->col,
141 component->asm_path, component->line,
142 component->note.empty() ? "" : " | ", component->note));
143 }
144 formatter.EndArray();
145
146 formatter.BeginArray("warnings_list");
147 for (const auto& warning : registry.warnings) {
148 formatter.AddArrayItem(warning);
149 }
150 formatter.EndArray();
151
152 return absl::OkStatus();
153}
154
156 Rom* /*rom*/, const resources::ArgumentParser& parser,
157 resources::OutputFormatter& formatter) {
158 ASSIGN_OR_RETURN(const int index, parser.GetInt("index"));
159 ASSIGN_OR_RETURN(const int row, parser.GetInt("row"));
160 ASSIGN_OR_RETURN(const int col, parser.GetInt("col"));
161
162 const std::filesystem::path project_path = ResolveProjectPath(parser);
163 ASSIGN_OR_RETURN(const auto root,
164 core::ResolveOracleProjectRoot(project_path));
165 const std::string asm_path = parser.GetString("asm").value_or("");
166 const std::string table_label = parser.GetString("table").value_or("");
167 const bool write_changes = parser.HasFlag("write");
168
170 project_path, asm_path, table_label, index,
171 row, col, write_changes));
172
173 formatter.AddField("project_root", root.string());
174 formatter.AddField("asm", result.asm_path);
175 formatter.AddField("line", result.line);
176 formatter.AddField("table", result.table_label);
177 formatter.AddField("index", result.index);
178 formatter.AddField("old_row", result.old_row);
179 formatter.AddField("old_col", result.old_col);
180 formatter.AddField("new_row", result.new_row);
181 formatter.AddField("new_col", result.new_col);
182 formatter.AddField("changed", result.changed);
183 formatter.AddField("write_applied", result.write_applied);
184 formatter.AddField("mode", write_changes ? "write" : "dry-run");
185 formatter.AddField("old_line", result.old_line);
186 formatter.AddField("new_line", result.new_line);
187
188 return absl::OkStatus();
189}
190
192 const resources::ArgumentParser& parser) {
193 if (auto max_row_str = parser.GetString("max-row"); max_row_str.has_value()) {
194 ASSIGN_OR_RETURN(const int max_row, parser.GetInt("max-row"));
195 if (max_row < 0) {
196 return absl::InvalidArgumentError("--max-row must be >= 0");
197 }
198 }
199 if (auto max_col_str = parser.GetString("max-col"); max_col_str.has_value()) {
200 ASSIGN_OR_RETURN(const int max_col, parser.GetInt("max-col"));
201 if (max_col < 0) {
202 return absl::InvalidArgumentError("--max-col must be >= 0");
203 }
204 }
205 return absl::OkStatus();
206}
207
209 Rom* /*rom*/, const resources::ArgumentParser& parser,
210 resources::OutputFormatter& formatter) {
211 const std::filesystem::path project_path = ResolveProjectPath(parser);
212 ASSIGN_OR_RETURN(const auto root,
213 core::ResolveOracleProjectRoot(project_path));
215
216 int max_row = 31;
217 int max_col = 31;
218 if (parser.GetString("max-row").has_value()) {
219 ASSIGN_OR_RETURN(max_row, parser.GetInt("max-row"));
220 }
221 if (parser.GetString("max-col").has_value()) {
222 ASSIGN_OR_RETURN(max_col, parser.GetInt("max-col"));
223 }
224 const bool strict = parser.HasFlag("strict");
225
227 core::ValidateOracleMenuRegistry(registry, max_row, max_col);
228 const bool failed = report.errors > 0 || (strict && report.warnings > 0);
229
230 formatter.AddField("project_root", root.string());
231 formatter.AddField("asm_files", static_cast<int>(registry.asm_files.size()));
232 formatter.AddField("bin_count", static_cast<int>(registry.bins.size()));
233 formatter.AddField("draw_routine_count",
234 static_cast<int>(registry.draw_routines.size()));
235 formatter.AddField("component_count", static_cast<int>(registry.components.size()));
236 formatter.AddField("max_row", max_row);
237 formatter.AddField("max_col", max_col);
238 formatter.AddField("strict", strict);
239 formatter.AddField("errors", report.errors);
240 formatter.AddField("warnings", report.warnings);
241 formatter.AddField("status", failed ? "fail" : "pass");
242
243 formatter.BeginArray("issues");
244 for (const auto& issue : report.issues) {
245 formatter.AddArrayItem(FormatIssueLine(issue));
246 }
247 formatter.EndArray();
248
249 if (failed) {
250 formatter.AddField("failure_reason", "Oracle menu validation failed");
251 return absl::FailedPreconditionError("Oracle menu validation failed");
252 }
253
254 return absl::OkStatus();
255}
256
257// ---------------------------------------------------------------------------
258// DungeonOraclePreflightCommandHandler::Execute
259// ---------------------------------------------------------------------------
260
262 const resources::ArgumentParser& parser) {
263 // Probe --report path writability here, before CommandHandler::Run() calls
264 // formatter.BeginObject(). This is the only hook guaranteed to run before
265 // any formatter output, so a failure produces true zero-stdout semantics:
266 // only stderr + non-zero exit, never "{}".
267 if (auto report_path_opt = parser.GetString("report");
268 report_path_opt.has_value() && !report_path_opt->empty()) {
269 const std::filesystem::path report_path(*report_path_opt);
270 std::error_code exists_ec;
271 const bool existed_before = std::filesystem::exists(report_path, exists_ec);
272
273 // Never use truncation in the probe path: validation must not clobber
274 // existing files if later argument parsing fails in Execute().
275 std::ofstream probe(*report_path_opt,
276 std::ios::out | std::ios::binary | std::ios::app);
277 if (!probe.is_open()) {
278 return absl::PermissionDeniedError(
279 absl::StrFormat("dungeon-oracle-preflight: cannot open report file "
280 "for writing: %s",
281 *report_path_opt));
282 }
283 probe.close();
284
285 // If the probe created a new file, remove it so ValidateArgs remains
286 // side-effect free on later parse failures.
287 if (!existed_before) {
288 std::error_code remove_ec;
289 std::filesystem::remove(report_path, remove_ec);
290 }
291 }
292 return absl::OkStatus();
293}
294
296 Rom* rom, const resources::ArgumentParser& parser,
297 resources::OutputFormatter& formatter) {
298 // --report path was already probed in ValidateArgs() (before the formatter
299 // was created), so a write failure here cannot produce contradictory output.
300
301 // Parse optional required-collision-rooms list.
302 std::vector<int> required_rooms;
303 if (auto rooms_opt = parser.GetString("required-collision-rooms");
304 rooms_opt.has_value()) {
305 for (absl::string_view token :
306 absl::StrSplit(rooms_opt.value(), ',', absl::SkipEmpty())) {
307 std::string trimmed =
308 std::string(absl::StripAsciiWhitespace(token));
309 int room_id = 0;
310 if (!util::ParseHexString(trimmed, &room_id)) {
311 return absl::InvalidArgumentError(absl::StrFormat(
312 "Invalid room ID in --required-collision-rooms: '%s'", trimmed));
313 }
314 required_rooms.push_back(room_id);
315 }
316 }
317
318 // Build preflight options from flags.
322 parser.HasFlag("require-write-support");
323 options.validate_water_fill_table = true;
325 !parser.HasFlag("skip-collision-maps");
326 options.room_ids_requiring_custom_collision = required_rooms;
327
328 // Run the preflight.
329 const auto preflight = zelda3::RunOracleRomSafetyPreflight(rom, options);
330
331 // Count per-check errors so we can report granular booleans.
332 bool water_fill_region_ok = true;
333 bool water_fill_table_ok = true;
334 bool custom_collision_maps_ok = true;
335 bool required_rooms_ok = true;
336 for (const auto& err : preflight.errors) {
337 if (err.code == "ORACLE_WATER_FILL_REGION_MISSING" ||
338 err.code == "ORACLE_COLLISION_WRITE_REGION_MISSING") {
339 water_fill_region_ok = false;
340 } else if (err.code == "ORACLE_WATER_FILL_HEADER_CORRUPT" ||
341 err.code == "ORACLE_WATER_FILL_TABLE_INVALID") {
342 water_fill_table_ok = false;
343 } else if (err.code == "ORACLE_COLLISION_POINTER_INVALID" ||
344 err.code == "ORACLE_COLLISION_POINTER_INVALID_TRUNCATED") {
345 custom_collision_maps_ok = false;
346 } else if (err.code == "ORACLE_REQUIRED_ROOM_MISSING_COLLISION" ||
347 err.code == "ORACLE_REQUIRED_ROOM_OUT_OF_RANGE") {
348 required_rooms_ok = false;
349 }
350 }
351
352 const bool failed = !preflight.ok();
353
354 // Determine whether the required-room check actually ran. It is gated on
355 // HasCustomCollisionWriteSupport in the preflight library, so on a small ROM
356 // the check is silently skipped. Reflect that honestly in the output.
357 const bool required_check_ran =
358 !required_rooms.empty() &&
360
361 // Build a machine-readable JSON report in parallel with the formatter so
362 // that --report writes the full structured output, not a reduced stub.
363 using json = nlohmann::json;
364 json report;
365 report["ok"] = !failed;
366 report["error_count"] = static_cast<int>(preflight.errors.size());
367 report["water_fill_region_ok"] = water_fill_region_ok;
368 report["water_fill_table_ok"] = water_fill_table_ok;
369 report["custom_collision_maps_ok"] = custom_collision_maps_ok;
370
371 if (!required_rooms.empty()) {
372 report["required_rooms_checked"] = static_cast<int>(required_rooms.size());
373 report["required_rooms_check"] = required_check_ran ? "ran" : "skipped";
374 if (required_check_ran) {
375 report["required_rooms_ok"] = required_rooms_ok;
376 }
377 }
378
379 json errors_arr = json::array();
380 for (const auto& err : preflight.errors) {
381 json entry;
382 entry["code"] = err.code;
383 entry["message"] = err.message;
384 entry["status"] = std::string(absl::StatusCodeToString(err.status_code));
385 if (err.room_id >= 0) {
386 entry["room_id"] = absl::StrFormat("0x%02X", err.room_id);
387 }
388 errors_arr.push_back(std::move(entry));
389 }
390 report["errors"] = std::move(errors_arr);
391 report["status"] = failed ? "fail" : "pass";
392
393 // Emit structured output to the formatter.
394 formatter.BeginObject("Dungeon Oracle Preflight");
395 formatter.AddField("ok", !failed);
396 formatter.AddField("error_count", static_cast<int>(preflight.errors.size()));
397 formatter.AddField("water_fill_region_ok", water_fill_region_ok);
398 formatter.AddField("water_fill_table_ok", water_fill_table_ok);
399 formatter.AddField("custom_collision_maps_ok", custom_collision_maps_ok);
400 if (!required_rooms.empty()) {
401 formatter.AddField("required_rooms_checked",
402 static_cast<int>(required_rooms.size()));
403 formatter.AddField("required_rooms_check",
404 required_check_ran ? std::string("ran")
405 : std::string("skipped"));
406 if (required_check_ran) {
407 formatter.AddField("required_rooms_ok", required_rooms_ok);
408 }
409 }
410 formatter.BeginArray("errors");
411 for (const auto& err : preflight.errors) {
412 formatter.BeginObject();
413 formatter.AddField("code", err.code);
414 formatter.AddField("message", err.message);
415 formatter.AddField("status",
416 std::string(absl::StatusCodeToString(err.status_code)));
417 if (err.room_id >= 0) {
418 formatter.AddHexField("room_id", err.room_id, 2);
419 }
420 formatter.EndObject();
421 }
422 formatter.EndArray();
423 formatter.AddField("status", failed ? "fail" : "pass");
424 formatter.EndObject();
425
426 // Write the full JSON report to a file if --report was given.
427 // Fail loudly on write errors so the caller knows the report is missing.
428 if (auto report_path = parser.GetString("report");
429 report_path.has_value() && !report_path->empty()) {
430 const std::string report_content = report.dump(2) + "\n";
431 std::ofstream report_file(*report_path,
432 std::ios::out | std::ios::binary | std::ios::trunc);
433 if (!report_file.is_open()) {
434 return absl::PermissionDeniedError(
435 absl::StrFormat("dungeon-oracle-preflight: cannot open report file "
436 "for writing: %s",
437 *report_path));
438 }
439 report_file << report_content;
440 if (!report_file.good()) {
441 return absl::InternalError(
442 absl::StrFormat("dungeon-oracle-preflight: failed while writing "
443 "report file: %s",
444 *report_path));
445 }
446 }
447
448 if (failed) {
449 return absl::FailedPreconditionError(
450 absl::StrFormat("Oracle ROM preflight failed (%d error(s))",
451 static_cast<int>(preflight.errors.size())));
452 }
453 return absl::OkStatus();
454}
455
456} // namespace yaze::cli::handlers
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
const auto & vector() const
Definition rom.h:143
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
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 BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::filesystem::path ResolveProjectPath(const resources::ArgumentParser &parser)
std::string FormatIssueLine(const core::OracleMenuValidationIssue &issue)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
std::string SeverityToString(DiagnosticSeverity severity)
Convert severity to string for output.
absl::StatusOr< std::filesystem::path > ResolveOracleProjectRoot(const std::filesystem::path &start_path)
absl::StatusOr< OracleMenuRegistry > BuildOracleMenuRegistry(const std::filesystem::path &project_root)
absl::StatusOr< OracleMenuComponentEditResult > SetOracleMenuComponentOffset(const std::filesystem::path &project_root, const std::string &asm_relative_path, const std::string &table_label, int index, int row, int col, bool write_changes)
OracleMenuValidationReport ValidateOracleMenuRegistry(const OracleMenuRegistry &registry, int max_row, int max_col)
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
OracleMenuValidationSeverity severity
std::vector< OracleMenuValidationIssue > issues