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