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"
16#include "nlohmann/json.hpp"
28 if (
auto project_opt = parser.
GetString(
"project"); project_opt.has_value()) {
29 return std::filesystem::path(*project_opt);
31 return std::filesystem::current_path();
35 return absl::StrFormat(
"%llu",
static_cast<unsigned long long>(size_bytes));
45 return absl::StrFormat(
"[%s] %s: %s (%s:%d)",
59 std::vector<int> room_ids;
60 const auto rooms_opt = parser.
GetString(flag_name);
61 if (!rooms_opt.has_value()) {
65 for (absl::string_view token :
66 absl::StrSplit(*rooms_opt,
',', absl::SkipEmpty())) {
67 const std::string trimmed = std::string(absl::StripAsciiWhitespace(token));
70 return absl::InvalidArgumentError(
71 absl::StrFormat(
"Invalid room ID in --%s: '%s'", flag_name, trimmed));
73 room_ids.push_back(room_id);
82 if (
auto table_filter = parser.
GetString(
"table");
83 table_filter.has_value() && table_filter->empty()) {
84 return absl::InvalidArgumentError(
"--table cannot be empty");
86 return absl::OkStatus();
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");
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) {
107 bins.push_back(&entry);
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) {
119 draw_routines.push_back(&routine);
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) {
128 components.push_back(&component);
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()));
140 for (
const auto* entry : bins) {
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"));
150 for (
const auto* routine : draw_routines) {
152 "%s | %s:%d | refs=%d%s", routine->label, routine->asm_path,
153 routine->line, routine->references, routine->local ?
" | local" :
""));
158 for (
const auto* component : components) {
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));
168 for (
const auto& warning : registry.warnings) {
173 return absl::OkStatus();
183 const std::filesystem::path project_path = ResolveProjectPath(parser);
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");
191 project_path, asm_path, table_label, index,
192 row, col, write_changes));
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);
209 return absl::OkStatus();
214 if (
auto max_row_str = parser.
GetString(
"max-row"); max_row_str.has_value()) {
217 return absl::InvalidArgumentError(
"--max-row must be >= 0");
220 if (
auto max_col_str = parser.
GetString(
"max-col"); max_col_str.has_value()) {
223 return absl::InvalidArgumentError(
"--max-col must be >= 0");
226 return absl::OkStatus();
232 const std::filesystem::path project_path = ResolveProjectPath(parser);
239 if (parser.
GetString(
"max-row").has_value()) {
242 if (parser.
GetString(
"max-col").has_value()) {
245 const bool strict = parser.
HasFlag(
"strict");
249 const bool failed = report.
errors > 0 || (strict && report.
warnings > 0);
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);
263 formatter.
AddField(
"status", failed ?
"fail" :
"pass");
266 for (
const auto& issue : report.
issues) {
272 formatter.
AddField(
"failure_reason",
"Oracle menu validation failed");
273 return absl::FailedPreconditionError(
"Oracle menu validation failed");
276 return absl::OkStatus();
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);
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 "
309 if (!existed_before) {
310 std::error_code remove_ec;
311 std::filesystem::remove(report_path, remove_ec);
314 return absl::OkStatus();
324 ParseHexRoomList(parser,
"required-collision-rooms"));
326 ParseHexRoomList(parser,
"required-water-fill-rooms"));
332 parser.
HasFlag(
"require-write-support");
335 !parser.
HasFlag(
"skip-collision-maps");
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;
366 if (!required_water_fill_rooms.empty() &&
367 (!water_fill_region_ok || !water_fill_table_ok)) {
368 required_water_fill_rooms_ok =
false;
371 const bool failed = !preflight.ok();
376 const bool required_check_ran =
377 !required_collision_rooms.empty() &&
382 using json = nlohmann::json;
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;
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;
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;
404 json errors_arr = json::array();
405 for (
const auto& err : preflight.errors) {
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);
413 errors_arr.push_back(std::move(entry));
415 report[
"errors"] = std::move(errors_arr);
416 report[
"status"] = failed ?
"fail" :
"pass";
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
430 : std::string(
"skipped"));
431 if (required_check_ran) {
432 formatter.
AddField(
"required_rooms_ok", required_rooms_ok);
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);
442 for (
const auto& err : preflight.errors) {
444 formatter.
AddField(
"code", err.code);
445 formatter.
AddField(
"message", err.message);
447 std::string(absl::StatusCodeToString(err.status_code)));
448 if (err.room_id >= 0) {
454 formatter.
AddField(
"status", failed ?
"fail" :
"pass");
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 "
470 report_file << report_content;
472 if (report_file.fail()) {
473 return absl::InternalError(
474 absl::StrFormat(
"dungeon-oracle-preflight: failed while writing "
481 return preflight.ToStatus();
483 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)
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)
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
std::vector< int > room_ids_requiring_water_fill_zones
bool require_custom_collision_write_support
bool require_water_fill_reserved_region
bool validate_water_fill_table