yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_validation_panel.cc
Go to the documentation of this file.
2
3#include <chrono>
4#include <fstream>
5#include <string>
6
7#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
12#include "imgui/imgui.h"
13#include "imgui/misc/cpp/imgui_stdlib.h"
14#include "rom/rom.h"
15#include "util/json.h"
16
17namespace yaze::editor {
18namespace {
19constexpr ImVec4 kGreen{0.2f, 0.75f, 0.3f, 1.0f};
20constexpr ImVec4 kRed{0.85f, 0.2f, 0.2f, 1.0f};
21constexpr ImVec4 kYellow{0.85f, 0.75f, 0.1f, 1.0f};
22constexpr ImVec4 kGrey{0.55f, 0.55f, 0.55f, 1.0f};
23
24ImVec4 BoolColor(bool flag) {
25 return flag ? kGreen : kRed;
26}
27
28const char* CheckStr(bool flag) {
29 return flag ? "OK" : "X";
30}
31
32void DrawCheckBadge(const std::string& state) {
33 if (state == "ran") {
34 ImGui::TextColored(kGreen, "[ran]");
35 return;
36 }
37 if (state == "skipped") {
38 ImGui::TextColored(kGrey, "[skipped]");
39 return;
40 }
41 ImGui::TextColored(kYellow, "[%s]", state.c_str());
42}
43
44void DrawOptionalBool(const char* label, const std::optional<bool>& flag) {
45 ImGui::Text(" %s", label);
46 ImGui::SameLine();
47 if (flag.has_value()) {
48 ImGui::TextColored(BoolColor(*flag), "%s", CheckStr(*flag));
49 return;
50 }
51 ImGui::TextColored(kGrey, "-");
52}
53
54bool LoadJsonSummaryFile(const std::string& path, Json* out) {
55 if (path.empty()) {
56 return false;
57 }
58 std::ifstream file(path);
59 if (!file.is_open()) {
60 return false;
61 }
62 try {
63 file >> *out;
64 return true;
65 } catch (...) {
66 return false;
67 }
68}
69
72 if (!project) {
73 return;
74 }
75
76 Json lint;
77 Json annotations;
78 Json hooks;
79 const bool has_lint =
80 LoadJsonSummaryFile(project->GetZ3dkArtifactPath("lint.json"), &lint);
81 const bool has_annotations = LoadJsonSummaryFile(
82 project->GetZ3dkArtifactPath("annotations.json"), &annotations);
83 const bool has_hooks =
84 LoadJsonSummaryFile(project->GetZ3dkArtifactPath("hooks.json"), &hooks);
85 if (!has_lint && !has_annotations && !has_hooks) {
86 return;
87 }
88
89 if (!ImGui::CollapsingHeader("z3dk Artifacts",
90 ImGuiTreeNodeFlags_DefaultOpen)) {
91 return;
92 }
93
94 if (has_lint) {
95 const int errors = lint.contains("errors") && lint["errors"].is_array()
96 ? static_cast<int>(lint["errors"].size())
97 : 0;
98 const int warnings =
99 lint.contains("warnings") && lint["warnings"].is_array()
100 ? static_cast<int>(lint["warnings"].size())
101 : 0;
102 ImGui::Text("Lint diagnostics: %d error(s), %d warning(s)", errors,
103 warnings);
104 }
105 if (has_annotations) {
106 const int count = annotations.contains("annotations") &&
107 annotations["annotations"].is_array()
108 ? static_cast<int>(annotations["annotations"].size())
109 : 0;
110 ImGui::Text("Annotations: %d", count);
111 }
112 if (has_hooks) {
113 const int count = hooks.contains("hooks") && hooks["hooks"].is_array()
114 ? static_cast<int>(hooks["hooks"].size())
115 : 0;
116 ImGui::Text("Hook/write blocks: %d", count);
117 }
118}
119} // namespace
120
122 if (pending_.valid()) {
123 pending_.wait();
124 }
125}
126
127std::string OracleValidationPanel::GetId() const {
128 return "oracle.validation";
129}
130
131std::string OracleValidationPanel::GetDisplayName() const {
132 return "Hack Validation";
133}
134
135std::string OracleValidationPanel::GetIcon() const {
137}
138
140 return "Agent";
141}
142
143std::string OracleValidationPanel::GetWorkflowGroup() const {
144 return "Validation";
145}
146
147std::string OracleValidationPanel::GetWorkflowLabel() const {
148 return "Hack Validation";
149}
150
152 return "Run project validation and readiness checks for the active hack";
153}
154
158 return project != nullptr && project->project_opened() && backend != nullptr;
159}
160
162 return "Validation backend is not available for the active hack project";
163}
164
167}
168
170 return 540.0f;
171}
172
173void OracleValidationPanel::Draw(bool* p_open) {
174 (void)p_open;
175 if (!IsEnabled()) {
176 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
177 return;
178 }
180 DrawOptions();
181 ImGui::Separator();
183 ImGui::Separator();
184 DrawResults();
185}
186
188 if (auto* project = ContentRegistry::Context::current_project();
189 project && !project->rom_filename.empty()) {
190 return project->rom_filename;
191 }
192 if (auto* rom = ContentRegistry::Context::rom(); rom && rom->is_loaded()) {
193 return rom->filename();
194 }
195 return "roms/zelda3.sfc";
196}
197
199 if (!running_) {
200 return;
201 }
202 if (pending_.wait_for(std::chrono::milliseconds(0)) !=
203 std::future_status::ready) {
204 return;
205 }
206 last_result_ = pending_.get();
207 running_ = false;
208 status_message_ = last_result_->command_ok ? "Completed." : "Failed.";
209}
210
213}
214
216 if (running_) {
217 return;
218 }
219
220 oracle_validation::SmokeOptions smoke_opts;
221 smoke_opts.rom_path = rom_path_;
222 smoke_opts.min_d6_track_rooms = min_d6_track_rooms_;
223 smoke_opts.strict_readiness =
225 if (write_report_ && !report_path_.empty()) {
226 smoke_opts.report_path = report_path_;
227 }
228
229 oracle_validation::PreflightOptions preflight_opts;
230 preflight_opts.rom_path = rom_path_;
231 preflight_opts.required_collision_rooms = required_collision_rooms_;
232 if (write_report_ && !report_path_.empty()) {
233 preflight_opts.report_path = report_path_;
234 }
235
236 Rom* rom_context = nullptr;
237 if (rom_path_.empty()) {
238 rom_context = GetRom();
239 }
240
241 running_ = true;
242 status_message_ = "Running...";
243
244 pending_ = std::async(std::launch::async, [mode, smoke_opts, preflight_opts,
245 rom_context]() {
247 return backend->RunValidation(mode, smoke_opts, preflight_opts,
248 rom_context);
249 }
250
251 oracle_validation::OracleRunResult result;
252 result.mode = mode;
253 result.timestamp = oracle_validation::CurrentTimestamp();
254 result.error_message = "No hack workflow backend registered";
255 result.status_code = absl::StatusCode::kFailedPrecondition;
256 return result;
257 });
258}
259
261 ImGui::SeparatorText("Options");
262
263 ImGui::SetNextItemWidth(320.0f);
264 ImGui::InputText("ROM Path", &rom_path_);
265 ImGui::SameLine();
266 if (ImGui::SmallButton("From ROM")) {
268 }
269
270 ImGui::SetNextItemWidth(80.0f);
271 ImGui::InputInt("Min D6 track rooms", &min_d6_track_rooms_);
272 if (min_d6_track_rooms_ < 0) {
274 }
275
276 ImGui::SeparatorText("Preflight options");
277 ImGui::SetNextItemWidth(200.0f);
278 ImGui::InputText("Required rooms", &required_collision_rooms_);
279 ImGui::SameLine();
280 ImGui::TextDisabled("(e.g. 0x25,0x27)");
281
282 ImGui::Checkbox("Write report file", &write_report_);
283 if (write_report_) {
284 ImGui::SetNextItemWidth(280.0f);
285 ImGui::InputText("Report path", &report_path_);
286 }
287}
288
290 Rom* rom = GetRom();
291 const bool rom_missing =
292 (rom == nullptr || !rom->is_loaded()) && rom_path_.empty();
293 if (rom_missing) {
294 ImGui::BeginDisabled();
295 }
296 if (running_) {
297 ImGui::BeginDisabled();
298 }
299
300 if (ImGui::Button("Run Structural Smoke")) {
302 }
303 ImGui::SameLine();
304 if (ImGui::Button("Run Strict Readiness")) {
306 }
307 ImGui::SameLine();
308 if (ImGui::Button("Run Oracle Preflight")) {
310 }
311
312 if (running_) {
313 ImGui::EndDisabled();
314 ImGui::SameLine();
315 ImGui::TextColored(kYellow, "%s", status_message_.c_str());
316 }
317 if (rom_missing) {
318 ImGui::EndDisabled();
319 ImGui::SameLine();
320 ImGui::TextColored(kRed, "Load a ROM first");
321 }
322}
323
325 if (!last_result_.has_value()) {
326 ImGui::TextDisabled("No results yet. Run a check above.");
327 return;
328 }
329 const auto& result = *last_result_;
330
331 const char* mode_label =
333 ? "Oracle Preflight"
335 ? "Strict Readiness Smoke"
336 : "Structural Smoke");
337 const bool overall_ok =
338 result.smoke.has_value()
339 ? result.smoke->ok
340 : (result.preflight.has_value() && result.preflight->ok);
341
342 ImGui::TextColored(overall_ok ? kGreen : kRed, "%s %s", CheckStr(overall_ok),
343 mode_label);
344 ImGui::SameLine(0.0f, 16.0f);
345 ImGui::TextColored(kGrey, "%s", result.timestamp.c_str());
346
347 ImGui::SetNextItemWidth(380.0f);
348 ImGui::InputText("##cli_cmd", const_cast<char*>(result.cli_command.c_str()),
349 result.cli_command.size() + 1, ImGuiInputTextFlags_ReadOnly);
350 ImGui::SameLine();
351 if (ImGui::SmallButton("Copy")) {
352 ImGui::SetClipboardText(result.cli_command.c_str());
353 }
354
355 if (!result.error_message.empty()) {
356 ImGui::TextColored(kRed, ICON_MD_ERROR " %s", result.error_message.c_str());
357 ImGui::TextDisabled(
358 "Hint: check that the ROM is loaded and the command is available.");
359 DrawRawOutput(result);
360 return;
361 }
362
363 if (result.json_parse_failed) {
364 ImGui::TextColored(kYellow,
365 ICON_MD_WARNING " JSON parse failed - raw output:");
366 DrawRawOutput(result);
367 return;
368 }
369
370 if (result.smoke.has_value()) {
371 DrawSmokeCards(*result.smoke);
372 }
373 if (result.preflight.has_value()) {
374 DrawPreflightCards(*result.preflight);
375 }
377}
378
380 const oracle_validation::SmokeResult& smoke) {
381 if (ImGui::CollapsingHeader("D4 Zora Temple",
382 ImGuiTreeNodeFlags_DefaultOpen)) {
383 ImGui::TextColored(BoolColor(smoke.d4.structural_ok), " Structural %s",
384 CheckStr(smoke.d4.structural_ok));
385 ImGui::Text(" Required rooms check:");
386 ImGui::SameLine();
387 DrawCheckBadge(smoke.d4.required_rooms_check);
388 DrawOptionalBool(" Rooms 0x25/0x27 have collision:",
389 smoke.d4.required_rooms_ok);
390 }
391
392 if (ImGui::CollapsingHeader("D6 Goron Mines",
393 ImGuiTreeNodeFlags_DefaultOpen)) {
394 ImGui::TextColored(
395 BoolColor(smoke.d6.meets_min_track_rooms), " Track rooms %d / %d %s",
396 smoke.d6.track_rooms_found, smoke.d6.min_track_rooms,
397 smoke.d6.meets_min_track_rooms ? "(ok)" : "(below threshold)");
398 ImGui::TextColored(BoolColor(smoke.d6.ok), " Audit command %s",
399 CheckStr(smoke.d6.ok));
400 }
401
402 if (ImGui::CollapsingHeader("D3 Kalyxo Castle",
403 ImGuiTreeNodeFlags_DefaultOpen)) {
404 ImGui::Text(" Readiness check:");
405 ImGui::SameLine();
406 DrawCheckBadge(smoke.d3.readiness_check);
407 DrawOptionalBool(" Room 0x32 has collision:", smoke.d3.ok);
408 }
409}
410
412 const oracle_validation::PreflightResult& preflight) {
413 if (ImGui::CollapsingHeader("Water Fill", ImGuiTreeNodeFlags_DefaultOpen)) {
414 ImGui::TextColored(BoolColor(preflight.water_fill_region_ok),
415 " Region present %s",
416 CheckStr(preflight.water_fill_region_ok));
417 ImGui::TextColored(BoolColor(preflight.water_fill_table_ok),
418 " Table valid %s",
419 CheckStr(preflight.water_fill_table_ok));
420 }
421
422 if (ImGui::CollapsingHeader("Custom Collision",
423 ImGuiTreeNodeFlags_DefaultOpen)) {
424 ImGui::TextColored(BoolColor(preflight.custom_collision_maps_ok),
425 " Maps valid %s",
426 CheckStr(preflight.custom_collision_maps_ok));
427 ImGui::Text(" Required rooms check:");
428 ImGui::SameLine();
429 DrawCheckBadge(preflight.required_rooms_check);
430 DrawOptionalBool(" Required rooms have data:",
431 preflight.required_rooms_ok);
432 }
433
434 if (!preflight.errors.empty()) {
435 const std::string errors_label = absl::StrFormat(
436 "Errors (%d)###preflight_errors", preflight.error_count);
437 if (ImGui::CollapsingHeader(errors_label.c_str())) {
438 for (const auto& err : preflight.errors) {
439 ImGui::TextColored(kRed, " [%s] %s", err.code.c_str(),
440 err.message.c_str());
441 if (err.room_id.has_value()) {
442 ImGui::SameLine();
443 ImGui::TextColored(kGrey, " room %s", err.room_id->c_str());
444 }
445 }
446 }
447 }
448}
449
451 const oracle_validation::OracleRunResult& result) {
452 if (ImGui::CollapsingHeader("Raw Output (diagnostics)")) {
453 ImGui::InputTextMultiline(
454 "##raw", const_cast<char*>(result.raw_output.c_str()),
455 result.raw_output.size() + 1, ImVec2(-1.0f, 120.0f),
456 ImGuiInputTextFlags_ReadOnly);
457 }
458}
459
460REGISTER_PANEL(OracleValidationPanel);
461
462} // namespace yaze::editor
auto filename() const
Definition rom.h:145
bool is_loaded() const
Definition rom.h:132
void DrawSmokeCards(const oracle_validation::SmokeResult &smoke)
std::optional< oracle_validation::OracleRunResult > last_result_
std::string GetWorkflowDescription() const override
Optional workflow description for menus/command palette.
void DrawRawOutput(const oracle_validation::OracleRunResult &result)
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
WindowLifecycle GetWindowLifecycle() const override
Get the lifecycle category for this window.
std::string GetId() const override
Unique identifier for this panel.
std::string GetWorkflowLabel() const override
Optional workflow label for menus/command palette.
void Draw(bool *p_open) override
Draw the panel content.
std::future< oracle_validation::OracleRunResult > pending_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
bool IsEnabled() const override
Check if this panel is currently enabled.
void LaunchRun(oracle_validation::RunMode mode)
std::string GetDisabledTooltip() const override
Get tooltip text when panel is disabled.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void DrawPreflightCards(const oracle_validation::PreflightResult &preflight)
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_VERIFIED_USER
Definition icons.h:2056
Rom * rom()
Get the current ROM instance.
::yaze::project::YazeProject * current_project()
Get the current project instance.
workflow::ValidationCapability * hack_validation_backend()
void DrawOptionalBool(const char *label, const std::optional< bool > &flag)
Editors are the view controllers for the application.
WindowLifecycle
Defines lifecycle behavior for editor windows.
@ CrossEditor
User can pin to persist across editors.
#define REGISTER_PANEL(PanelClass)
Auto-registration macro for panels with default constructors.