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#include "util/i18n/tr.h"
3
4#include <chrono>
5#include <fstream>
6#include <string>
7
8#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
13#include "imgui/imgui.h"
14#include "imgui/misc/cpp/imgui_stdlib.h"
15#include "rom/rom.h"
16#include "util/json.h"
17
18namespace yaze::editor {
19namespace {
20constexpr ImVec4 kGreen{0.2f, 0.75f, 0.3f, 1.0f};
21constexpr ImVec4 kRed{0.85f, 0.2f, 0.2f, 1.0f};
22constexpr ImVec4 kYellow{0.85f, 0.75f, 0.1f, 1.0f};
23constexpr ImVec4 kGrey{0.55f, 0.55f, 0.55f, 1.0f};
24
25ImVec4 BoolColor(bool flag) {
26 return flag ? kGreen : kRed;
27}
28
29const char* CheckStr(bool flag) {
30 return flag ? "OK" : "X";
31}
32
33void DrawCheckBadge(const std::string& state) {
34 if (state == "ran") {
35 ImGui::TextColored(kGreen, tr("[ran]"));
36 return;
37 }
38 if (state == "skipped") {
39 ImGui::TextColored(kGrey, tr("[skipped]"));
40 return;
41 }
42 ImGui::TextColored(kYellow, "[%s]", state.c_str());
43}
44
45void DrawOptionalBool(const char* label, const std::optional<bool>& flag) {
46 ImGui::Text(" %s", label);
47 ImGui::SameLine();
48 if (flag.has_value()) {
49 ImGui::TextColored(BoolColor(*flag), "%s", CheckStr(*flag));
50 return;
51 }
52 ImGui::TextColored(kGrey, "-");
53}
54
55bool LoadJsonSummaryFile(const std::string& path, Json* out) {
56 if (path.empty()) {
57 return false;
58 }
59 std::ifstream file(path);
60 if (!file.is_open()) {
61 return false;
62 }
63 try {
64 file >> *out;
65 return true;
66 } catch (...) {
67 return false;
68 }
69}
70
73 if (!project) {
74 return;
75 }
76
77 Json lint;
78 Json annotations;
79 Json hooks;
80 const bool has_lint =
81 LoadJsonSummaryFile(project->GetZ3dkArtifactPath("lint.json"), &lint);
82 const bool has_annotations = LoadJsonSummaryFile(
83 project->GetZ3dkArtifactPath("annotations.json"), &annotations);
84 const bool has_hooks =
85 LoadJsonSummaryFile(project->GetZ3dkArtifactPath("hooks.json"), &hooks);
86 if (!has_lint && !has_annotations && !has_hooks) {
87 return;
88 }
89
90 if (!ImGui::CollapsingHeader(tr("z3dk Artifacts"),
91 ImGuiTreeNodeFlags_DefaultOpen)) {
92 return;
93 }
94
95 if (has_lint) {
96 const int errors = lint.contains("errors") && lint["errors"].is_array()
97 ? static_cast<int>(lint["errors"].size())
98 : 0;
99 const int warnings =
100 lint.contains("warnings") && lint["warnings"].is_array()
101 ? static_cast<int>(lint["warnings"].size())
102 : 0;
103 ImGui::Text(tr("Lint diagnostics: %d error(s), %d warning(s)"), errors,
104 warnings);
105 }
106 if (has_annotations) {
107 const int count = annotations.contains("annotations") &&
108 annotations["annotations"].is_array()
109 ? static_cast<int>(annotations["annotations"].size())
110 : 0;
111 ImGui::Text(tr("Annotations: %d"), count);
112 }
113 if (has_hooks) {
114 const int count = hooks.contains("hooks") && hooks["hooks"].is_array()
115 ? static_cast<int>(hooks["hooks"].size())
116 : 0;
117 ImGui::Text(tr("Hook/write blocks: %d"), count);
118 }
119}
120} // namespace
121
123 if (pending_.valid()) {
124 pending_.wait();
125 }
126}
127
128std::string OracleValidationPanel::GetId() const {
129 return "oracle.validation";
130}
131
133 return "Hack Validation";
134}
135
138}
139
141 return "Agent";
142}
143
145 return "Validation";
146}
147
149 return "Hack Validation";
150}
151
153 return "Run project validation and readiness checks for the active hack";
154}
155
159 return project != nullptr && project->project_opened() && backend != nullptr;
160}
161
163 return "Validation backend is not available for the active hack project";
164}
165
169
171 return 540.0f;
172}
173
174void OracleValidationPanel::Draw(bool* p_open) {
175 (void)p_open;
176 if (!IsEnabled()) {
177 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
178 return;
179 }
181 DrawOptions();
182 ImGui::Separator();
184 ImGui::Separator();
185 DrawResults();
186}
187
189 if (auto* project = ContentRegistry::Context::current_project();
190 project && !project->rom_filename.empty()) {
191 return project->rom_filename;
192 }
193 if (auto* rom = ContentRegistry::Context::rom(); rom && rom->is_loaded()) {
194 return rom->filename();
195 }
196 return "roms/zelda3.sfc";
197}
198
200 if (!running_) {
201 return;
202 }
203 if (pending_.wait_for(std::chrono::milliseconds(0)) !=
204 std::future_status::ready) {
205 return;
206 }
207 last_result_ = pending_.get();
208 running_ = false;
209 status_message_ = last_result_->command_ok ? "Completed." : "Failed.";
210}
211
215
217 if (running_) {
218 return;
219 }
220
222 smoke_opts.rom_path = rom_path_;
224 smoke_opts.strict_readiness =
226 if (write_report_ && !report_path_.empty()) {
227 smoke_opts.report_path = report_path_;
228 }
229
231 preflight_opts.rom_path = rom_path_;
233 if (write_report_ && !report_path_.empty()) {
234 preflight_opts.report_path = report_path_;
235 }
236
237 Rom* rom_context = nullptr;
238 if (rom_path_.empty()) {
239 rom_context = GetRom();
240 }
241
242 running_ = true;
243 status_message_ = "Running...";
244
245 pending_ = std::async(std::launch::async, [mode, smoke_opts, preflight_opts,
246 rom_context]() {
248 return backend->RunValidation(mode, smoke_opts, preflight_opts,
249 rom_context);
250 }
251
253 result.mode = mode;
255 result.error_message = "No hack workflow backend registered";
256 result.status_code = absl::StatusCode::kFailedPrecondition;
257 return result;
258 });
259}
260
262 ImGui::SeparatorText(tr("Options"));
263
264 ImGui::SetNextItemWidth(320.0f);
265 ImGui::InputText(tr("ROM Path"), &rom_path_);
266 ImGui::SameLine();
267 if (ImGui::SmallButton(tr("From ROM"))) {
269 }
270
271 ImGui::SetNextItemWidth(80.0f);
272 ImGui::InputInt(tr("Min D6 track rooms"), &min_d6_track_rooms_);
273 if (min_d6_track_rooms_ < 0) {
275 }
276
277 ImGui::SeparatorText(tr("Preflight options"));
278 ImGui::SetNextItemWidth(200.0f);
279 ImGui::InputText(tr("Required rooms"), &required_collision_rooms_);
280 ImGui::SameLine();
281 ImGui::TextDisabled(tr("(e.g. 0x25,0x27)"));
282
283 ImGui::Checkbox(tr("Write report file"), &write_report_);
284 if (write_report_) {
285 ImGui::SetNextItemWidth(280.0f);
286 ImGui::InputText(tr("Report path"), &report_path_);
287 }
288}
289
291 Rom* rom = GetRom();
292 const bool rom_missing =
293 (rom == nullptr || !rom->is_loaded()) && rom_path_.empty();
294 if (rom_missing) {
295 ImGui::BeginDisabled();
296 }
297 if (running_) {
298 ImGui::BeginDisabled();
299 }
300
301 if (ImGui::Button(tr("Run Structural Smoke"))) {
303 }
304 ImGui::SameLine();
305 if (ImGui::Button(tr("Run Strict Readiness"))) {
307 }
308 ImGui::SameLine();
309 if (ImGui::Button(tr("Run Oracle Preflight"))) {
311 }
312
313 if (running_) {
314 ImGui::EndDisabled();
315 ImGui::SameLine();
316 ImGui::TextColored(kYellow, "%s", status_message_.c_str());
317 }
318 if (rom_missing) {
319 ImGui::EndDisabled();
320 ImGui::SameLine();
321 ImGui::TextColored(kRed, tr("Load a ROM first"));
322 }
323}
324
326 if (!last_result_.has_value()) {
327 ImGui::TextDisabled(tr("No results yet. Run a check above."));
328 return;
329 }
330 const auto& result = *last_result_;
331
332 const char* mode_label =
334 ? "Oracle Preflight"
336 ? "Strict Readiness Smoke"
337 : "Structural Smoke");
338 const bool overall_ok =
339 result.smoke.has_value()
340 ? result.smoke->ok
341 : (result.preflight.has_value() && result.preflight->ok);
342
343 ImGui::TextColored(overall_ok ? kGreen : kRed, "%s %s", CheckStr(overall_ok),
344 mode_label);
345 ImGui::SameLine(0.0f, 16.0f);
346 ImGui::TextColored(kGrey, "%s", result.timestamp.c_str());
347
348 ImGui::SetNextItemWidth(380.0f);
349 ImGui::InputText("##cli_cmd", const_cast<char*>(result.cli_command.c_str()),
350 result.cli_command.size() + 1, ImGuiInputTextFlags_ReadOnly);
351 ImGui::SameLine();
352 if (ImGui::SmallButton(tr("Copy"))) {
353 ImGui::SetClipboardText(result.cli_command.c_str());
354 }
355
356 if (!result.error_message.empty()) {
357 ImGui::TextColored(kRed, ICON_MD_ERROR " %s", result.error_message.c_str());
358 ImGui::TextDisabled(
359 tr("Hint: check that the ROM is loaded and the command is available."));
360 DrawRawOutput(result);
361 return;
362 }
363
364 if (result.json_parse_failed) {
365 ImGui::TextColored(kYellow,
366 ICON_MD_WARNING " JSON parse failed - raw output:");
367 DrawRawOutput(result);
368 return;
369 }
370
371 if (result.smoke.has_value()) {
372 DrawSmokeCards(*result.smoke);
373 }
374 if (result.preflight.has_value()) {
375 DrawPreflightCards(*result.preflight);
376 }
377 DrawZ3dkArtifactSummary();
378}
379
381 const oracle_validation::SmokeResult& smoke) {
382 if (ImGui::CollapsingHeader(tr("D4 Zora Temple"),
383 ImGuiTreeNodeFlags_DefaultOpen)) {
384 ImGui::TextColored(BoolColor(smoke.d4.structural_ok),
385 tr(" Structural %s"),
386 CheckStr(smoke.d4.structural_ok));
387 ImGui::Text(tr(" Required rooms check:"));
388 ImGui::SameLine();
389 DrawCheckBadge(smoke.d4.required_rooms_check);
390 DrawOptionalBool(" Rooms 0x25/0x27 have collision:",
391 smoke.d4.required_rooms_ok);
392 }
393
394 if (ImGui::CollapsingHeader(tr("D6 Goron Mines"),
395 ImGuiTreeNodeFlags_DefaultOpen)) {
396 ImGui::TextColored(
397 BoolColor(smoke.d6.meets_min_track_rooms),
398 tr(" Track rooms %d / %d %s"), smoke.d6.track_rooms_found,
399 smoke.d6.min_track_rooms,
400 smoke.d6.meets_min_track_rooms ? "(ok)" : "(below threshold)");
401 ImGui::TextColored(BoolColor(smoke.d6.ok), tr(" Audit command %s"),
402 CheckStr(smoke.d6.ok));
403 }
404
405 if (ImGui::CollapsingHeader(tr("D3 Kalyxo Castle"),
406 ImGuiTreeNodeFlags_DefaultOpen)) {
407 ImGui::Text(tr(" Readiness check:"));
408 ImGui::SameLine();
409 DrawCheckBadge(smoke.d3.readiness_check);
410 DrawOptionalBool(" Room 0x32 has collision:", smoke.d3.ok);
411 }
412}
413
415 const oracle_validation::PreflightResult& preflight) {
416 if (ImGui::CollapsingHeader(tr("Water Fill"),
417 ImGuiTreeNodeFlags_DefaultOpen)) {
418 ImGui::TextColored(BoolColor(preflight.water_fill_region_ok),
419 tr(" Region present %s"),
420 CheckStr(preflight.water_fill_region_ok));
421 ImGui::TextColored(BoolColor(preflight.water_fill_table_ok),
422 tr(" Table valid %s"),
423 CheckStr(preflight.water_fill_table_ok));
424 }
425
426 if (ImGui::CollapsingHeader(tr("Custom Collision"),
427 ImGuiTreeNodeFlags_DefaultOpen)) {
428 ImGui::TextColored(BoolColor(preflight.custom_collision_maps_ok),
429 tr(" Maps valid %s"),
430 CheckStr(preflight.custom_collision_maps_ok));
431 ImGui::Text(tr(" Required rooms check:"));
432 ImGui::SameLine();
433 DrawCheckBadge(preflight.required_rooms_check);
434 DrawOptionalBool(" Required rooms have data:",
435 preflight.required_rooms_ok);
436 }
437
438 if (!preflight.errors.empty()) {
439 const std::string errors_label = absl::StrFormat(
440 "Errors (%d)###preflight_errors", preflight.error_count);
441 if (ImGui::CollapsingHeader(errors_label.c_str())) {
442 for (const auto& err : preflight.errors) {
443 ImGui::TextColored(kRed, " [%s] %s", err.code.c_str(),
444 err.message.c_str());
445 if (err.room_id.has_value()) {
446 ImGui::SameLine();
447 ImGui::TextColored(kGrey, tr(" room %s"), err.room_id->c_str());
448 }
449 }
450 }
451 }
452}
453
456 if (ImGui::CollapsingHeader(tr("Raw Output (diagnostics)"))) {
457 ImGui::InputTextMultiline(
458 "##raw", const_cast<char*>(result.raw_output.c_str()),
459 result.raw_output.size() + 1, ImVec2(-1.0f, 120.0f),
460 ImGuiInputTextFlags_ReadOnly);
461 }
462}
463
465
466} // namespace yaze::editor
bool is_array() const
Definition json.h:58
size_t size() const
Definition json.h:61
bool contains(const std::string &) const
Definition json.h:53
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
auto filename() const
Definition rom.h:157
bool is_loaded() const
Definition rom.h:144
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.