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"
15#include "nlohmann/json.hpp"
27 if (
auto project_opt = parser.
GetString(
"project"); project_opt.has_value()) {
28 return std::filesystem::path(*project_opt);
30 return std::filesystem::current_path();
34 return absl::StrFormat(
"%llu",
static_cast<unsigned long long>(size_bytes));
44 return absl::StrFormat(
"[%s] %s: %s (%s:%d)",
49 return absl::StrFormat(
"[%s] %s: %s (%s)",
61 if (
auto table_filter = parser.
GetString(
"table");
62 table_filter.has_value() && table_filter->empty()) {
63 return absl::InvalidArgumentError(
"--table cannot be empty");
65 return absl::OkStatus();
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");
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) {
86 bins.push_back(&entry);
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) {
98 draw_routines.push_back(&routine);
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) {
107 components.push_back(&component);
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()));
119 for (
const auto* entry : bins) {
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"));
129 for (
const auto* routine : draw_routines) {
131 "%s | %s:%d | refs=%d%s", routine->label, routine->asm_path,
132 routine->line, routine->references, routine->local ?
" | local" :
""));
137 for (
const auto* component : components) {
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));
147 for (
const auto& warning : registry.warnings) {
152 return absl::OkStatus();
162 const std::filesystem::path project_path = ResolveProjectPath(parser);
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");
170 project_path, asm_path, table_label, index,
171 row, col, write_changes));
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);
188 return absl::OkStatus();
193 if (
auto max_row_str = parser.
GetString(
"max-row"); max_row_str.has_value()) {
196 return absl::InvalidArgumentError(
"--max-row must be >= 0");
199 if (
auto max_col_str = parser.
GetString(
"max-col"); max_col_str.has_value()) {
202 return absl::InvalidArgumentError(
"--max-col must be >= 0");
205 return absl::OkStatus();
211 const std::filesystem::path project_path = ResolveProjectPath(parser);
218 if (parser.
GetString(
"max-row").has_value()) {
221 if (parser.
GetString(
"max-col").has_value()) {
224 const bool strict = parser.
HasFlag(
"strict");
228 const bool failed = report.
errors > 0 || (strict && report.
warnings > 0);
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);
241 formatter.
AddField(
"status", failed ?
"fail" :
"pass");
244 for (
const auto& issue : report.
issues) {
250 formatter.
AddField(
"failure_reason",
"Oracle menu validation failed");
251 return absl::FailedPreconditionError(
"Oracle menu validation failed");
254 return absl::OkStatus();
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);
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 "
287 if (!existed_before) {
288 std::error_code remove_ec;
289 std::filesystem::remove(report_path, remove_ec);
292 return absl::OkStatus();
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));
311 return absl::InvalidArgumentError(absl::StrFormat(
312 "Invalid room ID in --required-collision-rooms: '%s'", trimmed));
314 required_rooms.push_back(room_id);
322 parser.
HasFlag(
"require-write-support");
325 !parser.
HasFlag(
"skip-collision-maps");
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;
352 const bool failed = !preflight.ok();
357 const bool required_check_ran =
358 !required_rooms.empty() &&
363 using json = nlohmann::json;
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;
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;
379 json errors_arr = json::array();
380 for (
const auto& err : preflight.errors) {
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);
388 errors_arr.push_back(std::move(entry));
390 report[
"errors"] = std::move(errors_arr);
391 report[
"status"] = failed ?
"fail" :
"pass";
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);
411 for (
const auto& err : preflight.errors) {
413 formatter.
AddField(
"code", err.code);
414 formatter.
AddField(
"message", err.message);
416 std::string(absl::StatusCodeToString(err.status_code)));
417 if (err.room_id >= 0) {
423 formatter.
AddField(
"status", failed ?
"fail" :
"pass");
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 "
439 report_file << report_content;
440 if (!report_file.good()) {
441 return absl::InternalError(
442 absl::StrFormat(
"dungeon-oracle-preflight: failed while writing "
449 return absl::FailedPreconditionError(
450 absl::StrFormat(
"Oracle ROM preflight failed (%d error(s))",
451 static_cast<int>(preflight.errors.size())));
453 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
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)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
std::string FormatSize(uintmax_t size_bytes)
std::filesystem::path ResolveProjectPath(const resources::ArgumentParser &parser)
std::string FormatIssueLine(const core::OracleMenuValidationIssue &issue)
bool ParseHexString(absl::string_view str, int *out)
std::string SeverityToString(DiagnosticSeverity severity)
Convert severity to string for output.
OracleMenuValidationSeverity
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 ®istry, int max_row, int max_col)
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
bool validate_custom_collision_maps
std::vector< int > room_ids_requiring_custom_collision
bool require_custom_collision_write_support
bool require_water_fill_reserved_region
bool validate_water_fill_table