yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_smoke_check_commands.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <ios>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
12#include "nlohmann/json.hpp"
13#include "rom/rom.h"
14#include "util/macro.h"
17
18namespace yaze::cli::handlers {
19
20using json = nlohmann::json;
21
24 Descriptor desc;
25 desc.display_name = "Oracle Smoke Check";
26 desc.summary =
27 "Single-shot Oracle ROM smoke check covering D4 Zora Temple (water "
28 "system), D6 Goron Mines (minecart rooms 0xA8/0xB8/0xD8/0xDA), and "
29 "D3 Kalyxo Castle (prison room 0x32 collision readiness). Structural "
30 "failures always exit non-zero; readiness gaps are informational unless "
31 "--strict-readiness is set.";
32 desc.todo_reference = "todo#oracle-testing-infra";
33 desc.entries = {
34 {"--strict-readiness",
35 "Also fail when D4 rooms 0x25/0x27 or D3 room 0x32 lack authored "
36 "custom collision data",
37 ""},
38 {"--min-d6-track-rooms",
39 "Fail if fewer than N of the 4 D6 rooms have track rail objects "
40 "(default 0 = informational only). Treated as structural failure.",
41 ""},
42 {"--report", "Write full JSON summary to this path in addition to stdout",
43 ""},
44 };
45 return desc;
46}
47
49 const resources::ArgumentParser& parser) {
50 // Validate --min-d6-track-rooms if provided.
51 if (parser.GetString("min-d6-track-rooms").has_value()) {
52 auto int_or = parser.GetInt("min-d6-track-rooms");
53 if (!int_or.ok()) {
54 return absl::InvalidArgumentError(
55 "--min-d6-track-rooms: value must be a non-negative integer");
56 }
57 if (*int_or < 0) {
58 return absl::InvalidArgumentError(
59 "--min-d6-track-rooms: value must be >= 0");
60 }
61 }
62
63 // Probe --report path before the formatter opens. Failure here emits nothing
64 // to stdout (only stderr + non-zero exit), preserving the contract that
65 // stdout is valid iff exit code is 0.
66 if (auto rp = parser.GetString("report");
67 rp.has_value() && !rp->empty()) {
68 const std::filesystem::path rp_path(*rp);
69 std::error_code ec;
70 const bool existed_before = std::filesystem::exists(rp_path, ec);
71 std::ofstream probe(*rp, std::ios::out | std::ios::binary | std::ios::app);
72 if (!probe.is_open()) {
73 return absl::PermissionDeniedError(
74 absl::StrFormat("oracle-smoke-check: cannot open report file "
75 "for writing: %s",
76 *rp));
77 }
78 probe.close();
79 if (!existed_before) {
80 std::filesystem::remove(rp_path, ec);
81 }
82 }
83 return absl::OkStatus();
84}
85
87 Rom* rom, const resources::ArgumentParser& parser,
88 resources::OutputFormatter& formatter) {
89 const bool strict_readiness = parser.HasFlag("strict-readiness");
90
91 // Parse --min-d6-track-rooms (default 0 = informational only).
92 int min_d6_track_rooms = 0;
93 if (auto int_or = parser.GetInt("min-d6-track-rooms"); int_or.ok()) {
94 min_d6_track_rooms = *int_or;
95 }
96
97 // ------------------------------------------------------------------
98 // D4 Zora Temple — structural preflight
99 // ------------------------------------------------------------------
100 // Validates water-fill reserved region and table. Does NOT require specific
101 // room collision data (that's a readiness check, below).
103 structural_opts.require_water_fill_reserved_region = true;
104 structural_opts.require_custom_collision_write_support = false;
105 structural_opts.validate_water_fill_table = true;
106 structural_opts.validate_custom_collision_maps = false;
107 const auto structural =
108 zelda3::RunOracleRomSafetyPreflight(rom, structural_opts);
109 const bool d4_structural_ok = structural.ok();
110
111 // Whether the ROM's custom-collision write region exists determines if
112 // required-room checks can actually run. Mirror the preflight library's
113 // gate (HasCustomCollisionWriteSupport) so we report "ran" vs "skipped"
114 // rather than silently claiming readiness=true on non-expanded ROMs.
115 const bool collision_write_support =
117
118 // ------------------------------------------------------------------
119 // D4 Zora Temple — room collision readiness (informational by default)
120 // ------------------------------------------------------------------
121 bool d4_required_ok = false;
122 const char* d4_readiness_state = "skipped";
123 if (collision_write_support) {
126 d4_opts.validate_water_fill_table = false;
127 d4_opts.validate_custom_collision_maps = false;
128 d4_opts.room_ids_requiring_custom_collision = {0x25, 0x27};
129 const auto d4_required =
131 d4_required_ok = d4_required.ok();
132 d4_readiness_state = "ran";
133 }
134
135 // ------------------------------------------------------------------
136 // D6 Goron Mines — minecart audit on fixed room set
137 // ------------------------------------------------------------------
138 // The audit always exits ok (issues are informational). d6_ok can only be
139 // false if the ROM itself is unreadable (which would have been caught by the
140 // structural check above).
141 DungeonMinecartAuditCommandHandler minecart_handler;
142 std::string d6_out;
143 const bool d6_ok =
144 minecart_handler
145 .Run({"--rooms=0xA8,0xB8,0xD8,0xDA", "--include-track-objects",
146 "--format=json"},
147 rom, &d6_out)
148 .ok();
149
150 // Count D6 rooms with non-empty track_object_subtypes from audit output.
151 int track_rooms_found = 0;
152 {
153 const auto d6_doc = json::parse(d6_out, nullptr, false);
154 if (!d6_doc.is_discarded() &&
155 d6_doc.contains("Dungeon Minecart Audit")) {
156 for (const auto& room :
157 d6_doc["Dungeon Minecart Audit"].value("rooms", json::array())) {
158 if (!room.value("track_object_subtypes", json::array()).empty()) {
159 ++track_rooms_found;
160 }
161 }
162 }
163 }
164 const bool meets_min_track_rooms =
165 (min_d6_track_rooms == 0 || track_rooms_found >= min_d6_track_rooms);
166
167 // ------------------------------------------------------------------
168 // D3 Kalyxo Castle — prison room collision readiness
169 // ------------------------------------------------------------------
170 bool d3_ok = false;
171 const char* d3_readiness_state = "skipped";
172 if (collision_write_support) {
175 d3_opts.validate_water_fill_table = false;
176 d3_opts.validate_custom_collision_maps = false;
178 const auto d3_required =
180 d3_ok = d3_required.ok();
181 d3_readiness_state = "ran";
182 }
183
184 // ------------------------------------------------------------------
185 // Aggregate
186 // ------------------------------------------------------------------
187 // meets_min_track_rooms is structural (not readiness) so it's always gated.
188 bool overall_ok = d4_structural_ok && d6_ok && meets_min_track_rooms;
189 if (strict_readiness) {
190 // Only apply readiness gates if the checks actually ran. A "skipped" check
191 // on a non-expanded ROM is not a readiness failure — it's a precondition
192 // failure, already surfaced by d4_structural_ok = false.
193 if (collision_write_support) {
194 overall_ok = overall_ok && d4_required_ok && d3_ok;
195 }
196 }
197
198 // ------------------------------------------------------------------
199 // Build parallel JSON report (for --report file)
200 // ------------------------------------------------------------------
201 json report;
202 report["ok"] = overall_ok;
203 report["status"] = overall_ok ? "pass" : "fail";
204 report["strict_readiness"] = strict_readiness;
205
206 json d4_json = json::object();
207 d4_json["structural_ok"] = d4_structural_ok;
208 d4_json["required_rooms_check"] = d4_readiness_state;
209 if (collision_write_support) {
210 d4_json["required_rooms_ok"] = d4_required_ok;
211 }
212 d4_json["required_rooms"] = json::array({"0x25", "0x27"});
213 json structural_errors = json::array();
214 for (const auto& err : structural.errors) {
215 structural_errors.push_back({{"code", err.code}, {"message", err.message}});
216 }
217 d4_json["structural_errors"] = std::move(structural_errors);
218
219 json d6_json = json::object();
220 d6_json["ok"] = d6_ok;
221 d6_json["rooms"] = json::array({"0xA8", "0xB8", "0xD8", "0xDA"});
222 d6_json["track_rooms_found"] = track_rooms_found;
223 d6_json["min_track_rooms"] = min_d6_track_rooms;
224 d6_json["meets_min_track_rooms"] = meets_min_track_rooms;
225
226 json d3_json = json::object();
227 d3_json["readiness_check"] = d3_readiness_state;
228 if (collision_write_support) {
229 d3_json["ok"] = d3_ok;
230 }
231 d3_json["required_rooms"] = json::array({"0x32"});
232
233 report["checks"] = json::object();
234 report["checks"]["d4_zora_temple"] = std::move(d4_json);
235 report["checks"]["d6_goron_mines"] = std::move(d6_json);
236 report["checks"]["d3_kalyxo_castle"] = std::move(d3_json);
237
238 // ------------------------------------------------------------------
239 // Emit to formatter
240 // ------------------------------------------------------------------
241 formatter.BeginObject("Oracle Smoke Check");
242 formatter.AddField("ok", overall_ok);
243 formatter.AddField("status",
244 overall_ok ? std::string("pass") : std::string("fail"));
245 formatter.AddField("strict_readiness", strict_readiness);
246
247 formatter.BeginObject("checks");
248
249 formatter.BeginObject("d4_zora_temple");
250 formatter.AddField("structural_ok", d4_structural_ok);
251 formatter.AddField("required_rooms_check",
252 std::string(d4_readiness_state));
253 if (collision_write_support) {
254 formatter.AddField("required_rooms_ok", d4_required_ok);
255 }
256 formatter.EndObject();
257
258 formatter.BeginObject("d6_goron_mines");
259 formatter.AddField("ok", d6_ok);
260 formatter.AddField("track_rooms_found", track_rooms_found);
261 formatter.AddField("min_track_rooms", min_d6_track_rooms);
262 formatter.AddField("meets_min_track_rooms", meets_min_track_rooms);
263 formatter.EndObject();
264
265 formatter.BeginObject("d3_kalyxo_castle");
266 formatter.AddField("readiness_check", std::string(d3_readiness_state));
267 if (collision_write_support) {
268 formatter.AddField("ok", d3_ok);
269 }
270 formatter.EndObject();
271
272 formatter.EndObject(); // close "checks"
273 formatter.EndObject(); // close "Oracle Smoke Check"
274
275 // ------------------------------------------------------------------
276 // Write report file (always written, pass or fail)
277 // ------------------------------------------------------------------
278 if (auto rp = parser.GetString("report");
279 rp.has_value() && !rp->empty()) {
280 std::ofstream report_file(
281 *rp, std::ios::out | std::ios::binary | std::ios::trunc);
282 if (!report_file.is_open()) {
283 return absl::PermissionDeniedError(absl::StrFormat(
284 "oracle-smoke-check: cannot open report file: %s", *rp));
285 }
286 report_file << report.dump(2) << "\n";
287 if (!report_file.good()) {
288 return absl::InternalError(absl::StrFormat(
289 "oracle-smoke-check: failed writing report: %s", *rp));
290 }
291 }
292
293 if (!overall_ok) {
294 // Attribute the failure accurately: structural (d4/d6/threshold always
295 // evaluated) vs readiness-only (only possible in strict mode after
296 // structural passed).
297 const bool structural_failed =
298 !d4_structural_ok || !d6_ok || !meets_min_track_rooms;
299 return absl::FailedPreconditionError(absl::StrFormat(
300 "oracle-smoke-check failed %s",
301 structural_failed ? "(structural)" : "(strict-readiness)"));
302 }
303 return absl::OkStatus();
304}
305
306} // 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
Descriptor Describe() const override
Provide metadata for TUI/help summaries.
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.
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)
absl::Status Run(const std::vector< std::string > &args, Rom *rom_context, std::string *captured_output=nullptr)
Execute the command.
Utility for consistent output formatting across commands.
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.
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)