yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_state_library_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdio>
5#include <cstring>
6#include <fstream>
7#include <sstream>
8
9#ifdef _WIN32
10#define popen _popen
11#define pclose _pclose
12#endif
13
14#include "absl/strings/str_format.h"
15#include "absl/time/clock.h"
16#include "absl/time/time.h"
20#include "app/gui/core/icons.h"
21#include "imgui/imgui.h"
22#include "nlohmann/json.hpp"
23
24namespace yaze {
25namespace editor {
26
27namespace {
28
29// Status colors
30ImVec4 GetStatusColor(const std::string& status) {
31 if (status == "canon") {
32 return ImVec4(0.2f, 0.8f, 0.2f, 1.0f); // Green
33 } else if (status == "draft") {
34 return ImVec4(0.9f, 0.7f, 0.1f, 1.0f); // Yellow
35 } else if (status == "deprecated") {
36 return ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Gray
37 }
38 return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
39}
40
41std::string GetStatusBadge(const std::string& status) {
42 if (status == "canon")
43 return "[CANON]";
44 if (status == "draft")
45 return "[draft]";
46 if (status == "deprecated")
47 return "[DEPR]";
48 return "[???]";
49}
50
51} // namespace
52
54 // Default Oracle manifest path
55 const char* home = std::getenv("HOME");
56 if (home) {
57 manifest_path_ = absl::StrFormat(
58 "%s/src/hobby/oracle-of-secrets/Docs/Testing/save_state_library.json",
59 home);
60 }
62}
63
65
67 std::shared_ptr<emu::mesen::MesenSocketClient> client) {
68 client_ = std::move(client);
69}
70
73 status_message_ = absl::StrFormat("Loaded %d states", entries_.size());
74 status_is_error_ = false;
75}
76
78 entries_.clear();
79
80 std::ifstream file(manifest_path_);
81 if (!file.is_open()) {
82 status_message_ = "Failed to open manifest: " + manifest_path_;
83 status_is_error_ = true;
84 return;
85 }
86
87 try {
88 nlohmann::json data = nlohmann::json::parse(file);
89
90 if (data.contains("library_root")) {
91 library_root_ = data["library_root"].get<std::string>();
92 }
93
94 if (data.contains("entries") && data["entries"].is_array()) {
95 for (const auto& entry_json : data["entries"]) {
96 StateEntry entry;
97 entry.id = entry_json.value("id", "");
98 entry.label =
99 entry_json.value("label", entry_json.value("description", ""));
100 entry.path = entry_json.value("path", "");
101 entry.status = entry_json.value("status", "draft");
102 entry.md5 = entry_json.value("md5", "");
103 entry.captured_by = entry_json.value("captured_by", "");
104 entry.verified_by = entry_json.value("verified_by", "");
105 entry.verified_at = entry_json.value("verified_at", "");
106 entry.deprecated_reason = entry_json.value("deprecated_reason", "");
107
108 // Tags
109 if (entry_json.contains("tags") && entry_json["tags"].is_array()) {
110 for (const auto& tag : entry_json["tags"]) {
111 entry.tags.push_back(tag.get<std::string>());
112 }
113 }
114
115 // Metadata
116 if (entry_json.contains("metadata")) {
117 const auto& meta = entry_json["metadata"];
118 entry.module = meta.value("module", 0);
119 entry.room = meta.value("room", 0);
120 entry.area = meta.value("area", 0);
121 entry.indoors = meta.value("indoors", false);
122 entry.link_x = meta.value("link_x", 0);
123 entry.link_y = meta.value("link_y", 0);
124 entry.health = meta.value("health", 0);
125 entry.max_health = meta.value("max_health", 0);
126 entry.rupees = meta.value("rupees", 0);
127 entry.location = meta.value("location", "");
128 entry.summary = meta.value("summary", "");
129 } else if (entry_json.contains("gameState")) {
130 // Legacy format
131 const auto& gs = entry_json["gameState"];
132 entry.indoors = gs.value("indoors", false);
133 }
134
135 entries_.push_back(entry);
136 }
137 }
138
139 status_message_ = absl::StrFormat("Loaded %d states", entries_.size());
140 status_is_error_ = false;
141 } catch (const std::exception& e) {
142 status_message_ = absl::StrFormat("JSON parse error: %s", e.what());
143 status_is_error_ = true;
144 }
145}
146
148 // Re-read the full manifest, update entries, and write back
149 std::ifstream file_in(manifest_path_);
150 if (!file_in.is_open()) {
151 status_message_ = "Failed to open manifest for writing";
152 status_is_error_ = true;
153 return;
154 }
155
156 nlohmann::json data;
157 try {
158 data = nlohmann::json::parse(file_in);
159 } catch (...) {
160 status_message_ = "Failed to parse manifest for update";
161 status_is_error_ = true;
162 return;
163 }
164 file_in.close();
165
166 // Update entries in the JSON
167 if (data.contains("entries") && data["entries"].is_array()) {
168 for (auto& entry_json : data["entries"]) {
169 std::string id = entry_json.value("id", "");
170 for (const auto& entry : entries_) {
171 if (entry.id == id) {
172 entry_json["status"] = entry.status;
173 if (!entry.verified_by.empty()) {
174 entry_json["verified_by"] = entry.verified_by;
175 entry_json["verified_at"] = entry.verified_at;
176 }
177 if (!entry.deprecated_reason.empty()) {
178 entry_json["deprecated_reason"] = entry.deprecated_reason;
179 }
180 break;
181 }
182 }
183 }
184 }
185
186 std::ofstream file_out(manifest_path_);
187 if (!file_out.is_open()) {
188 status_message_ = "Failed to write manifest";
189 status_is_error_ = true;
190 return;
191 }
192 file_out << data.dump(2);
193 status_message_ = "Manifest saved";
194 status_is_error_ = false;
195}
196
197absl::Status OracleStateLibraryPanel::LoadState(const std::string& state_id) {
198 // Find the entry
199 const StateEntry* entry = nullptr;
200 for (const auto& ent : entries_) {
201 if (ent.id == state_id) {
202 entry = &ent;
203 break;
204 }
205 }
206 if (!entry) {
207 return absl::NotFoundError("State not found: " + state_id);
208 }
209
210 // Use the Python CLI to load the state (supports path-based loading)
211 const char* home = std::getenv("HOME");
212 std::string socket_arg;
213 if (client_ && client_->IsConnected()) {
214 std::string path = client_->GetSocketPath();
215 std::string escaped;
216 escaped.reserve(path.size() + 2);
217 escaped += '"';
218 for (char c : path) {
219 if (c == '\\' || c == '"')
220 escaped += '\\';
221 escaped += c;
222 }
223 escaped += '"';
224 socket_arg = " --socket " + escaped;
225 }
226 std::string cmd = absl::StrFormat(
227 "python3 %s/src/hobby/oracle-of-secrets/scripts/mesen2_client.py%s "
228 "lib-load %s 2>&1",
229 home ? home : "", socket_arg, state_id.c_str());
230
231 FILE* pipe = popen(cmd.c_str(), "r");
232 if (!pipe) {
233 return absl::InternalError("Failed to execute lib-load command");
234 }
235
236 char buffer[256];
237 std::string output;
238 while (fgets(buffer, sizeof(buffer), pipe)) {
239 output += buffer;
240 }
241 int ret = pclose(pipe);
242
243 if (ret != 0) {
244 return absl::InternalError("lib-load failed: " + output);
245 }
246
247 status_message_ = absl::StrFormat("Loaded: %s", entry->label.c_str());
248 status_is_error_ = false;
249 return absl::OkStatus();
250}
251
252absl::Status OracleStateLibraryPanel::VerifyState(const std::string& state_id) {
253 for (auto& entry : entries_) {
254 if (entry.id == state_id) {
255 entry.status = "canon";
256 entry.verified_by = "scawful"; // TODO(scawful): Make configurable
257 entry.verified_at = absl::FormatTime(absl::RFC3339_full, absl::Now(),
258 absl::UTCTimeZone());
259 SaveManifest();
260 status_message_ = absl::StrFormat("Verified: %s", entry.label.c_str());
261 status_is_error_ = false;
262 return absl::OkStatus();
263 }
264 }
265 return absl::NotFoundError("State not found: " + state_id);
266}
267
269 const std::string& state_id, const std::string& reason) {
270 for (auto& entry : entries_) {
271 if (entry.id == state_id) {
272 entry.status = "deprecated";
273 entry.deprecated_reason = reason;
274 SaveManifest();
275 status_message_ = absl::StrFormat("Deprecated: %s", entry.label.c_str());
276 status_is_error_ = false;
277 return absl::OkStatus();
278 }
279 }
280 return absl::NotFoundError("State not found: " + state_id);
281}
282
284 ImGui::PushID("OracleStateLibraryPanel");
285
286 DrawToolbar();
287 ImGui::Separator();
288
289 // Main content area
290 float details_width = 300.0f;
291 ImVec2 avail = ImGui::GetContentRegionAvail();
292
293 // Left side: state list
294 ImGui::BeginChild("StateList", ImVec2(avail.x - details_width - 10, 0), true);
296 ImGui::EndChild();
297
298 ImGui::SameLine();
299
300 // Right side: details
301 ImGui::BeginChild("StateDetails", ImVec2(details_width, 0), true);
303 ImGui::EndChild();
304
305 // Dialogs
307
308 ImGui::PopID();
309}
310
312 // Refresh button
313 if (ImGui::Button(ICON_MD_REFRESH " Refresh")) {
315 }
316 ImGui::SameLine();
317
318 // Filter toggles
319 ImGui::Checkbox("Canon", &show_canon_);
320 ImGui::SameLine();
321 ImGui::Checkbox("Draft", &show_draft_);
322 ImGui::SameLine();
323 ImGui::Checkbox("Deprecated", &show_deprecated_);
324 ImGui::SameLine();
325
326 // Text filter
327 ImGui::SetNextItemWidth(150);
328 ImGui::InputTextWithHint("##filter", "Filter...", filter_text_,
329 sizeof(filter_text_));
330
331 // Status message
332 if (!status_message_.empty()) {
333 ImGui::SameLine();
334 ImGui::TextColored(
335 status_is_error_ ? ImVec4(1, 0.3f, 0.3f, 1) : ImVec4(0.3f, 1, 0.3f, 1),
336 "%s", status_message_.c_str());
337 }
338
339 // Stats
340 int canon_count = 0, draft_count = 0, depr_count = 0;
341 for (const auto& e : entries_) {
342 if (e.status == "canon")
343 canon_count++;
344 else if (e.status == "draft")
345 draft_count++;
346 else if (e.status == "deprecated")
347 depr_count++;
348 }
349 ImGui::SameLine();
350 ImGui::TextDisabled("| %d canon, %d draft, %d deprecated", canon_count,
351 draft_count, depr_count);
352}
353
355 std::string filter_lower(filter_text_);
356 std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
357 ::tolower);
358
359 int visible_index = 0;
360 for (size_t i = 0; i < entries_.size(); ++i) {
361 const auto& entry = entries_[i];
362
363 // Filter by status
364 if (entry.status == "canon" && !show_canon_)
365 continue;
366 if (entry.status == "draft" && !show_draft_)
367 continue;
368 if (entry.status == "deprecated" && !show_deprecated_)
369 continue;
370
371 // Filter by text
372 if (!filter_lower.empty()) {
373 std::string label_lower = entry.label;
374 std::transform(label_lower.begin(), label_lower.end(),
375 label_lower.begin(), ::tolower);
376 std::string id_lower = entry.id;
377 std::transform(id_lower.begin(), id_lower.end(), id_lower.begin(),
378 ::tolower);
379 if (label_lower.find(filter_lower) == std::string::npos &&
380 id_lower.find(filter_lower) == std::string::npos) {
381 continue;
382 }
383 }
384
385 // Status badge
386 ImGui::TextColored(GetStatusColor(entry.status), "%s",
387 GetStatusBadge(entry.status).c_str());
388 ImGui::SameLine();
389
390 // Selectable
391 bool is_selected = (selected_index_ == static_cast<int>(i));
392 if (ImGui::Selectable(entry.label.c_str(), is_selected,
393 ImGuiSelectableFlags_SpanAllColumns)) {
394 selected_index_ = static_cast<int>(i);
395 }
396
397 // Context menu
398 gui::ItemContextMenu(nullptr, [&]() {
399 const std::string id = entry.id;
400 std::vector<gui::MenuItemSpec> items;
401 items.emplace_back("Load in Mesen2", ICON_MD_PLAY_ARROW, [this, id]() {
402 auto status = LoadState(id);
403 if (!status.ok()) {
404 status_message_ = std::string(status.message());
405 status_is_error_ = true;
406 }
407 });
408 items.back().separator_after = true;
409 items.push_back(gui::CopyToClipboardItem("Copy State ID", id));
410 if (entry.status == "draft") {
411 items.emplace_back("Verify as Canon...", ICON_MD_CHECK, [this, id]() {
413 show_verify_dialog_ = true;
414 });
415 }
416 if (entry.status != "deprecated") {
417 items.back().separator_after = true;
418 // Opens the deprecate dialog, which does its own confirmation.
419 items.push_back(gui::MenuItemSpec::Destructive(
420 "Deprecate...", ICON_MD_DELETE,
421 [this, id]() {
424 },
425 /*confirm=*/false));
426 }
427 return items;
428 });
429
430 // Tooltip with tags
431 if (ImGui::IsItemHovered() && !entry.tags.empty()) {
432 ImGui::BeginTooltip();
433 ImGui::Text("Tags: ");
434 for (size_t t = 0; t < entry.tags.size(); ++t) {
435 if (t > 0)
436 ImGui::SameLine();
437 ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "[%s]",
438 entry.tags[t].c_str());
439 }
440 ImGui::EndTooltip();
441 }
442
443 ++visible_index;
444 }
445}
446
448 if (selected_index_ < 0 ||
449 selected_index_ >= static_cast<int>(entries_.size())) {
450 ImGui::TextDisabled("Select a state to view details");
451 return;
452 }
453
454 const auto& entry = entries_[selected_index_];
455
456 // Header
457 ImGui::TextColored(GetStatusColor(entry.status), "%s",
458 GetStatusBadge(entry.status).c_str());
459 ImGui::SameLine();
460 ImGui::Text("%s", entry.label.c_str());
461 ImGui::Separator();
462
463 // Actions
464 bool connected = client_ && client_->IsConnected();
465 if (!connected) {
466 ImGui::TextDisabled("Connect to Mesen2 to load states");
467 } else {
468 if (ImGui::Button(ICON_MD_PLAY_ARROW " Load", ImVec2(-1, 0))) {
469 auto status = LoadState(entry.id);
470 if (!status.ok()) {
471 status_message_ = std::string(status.message());
472 status_is_error_ = true;
473 }
474 }
475 }
476
477 if (entry.status == "draft") {
478 if (ImGui::Button(ICON_MD_CHECK " Verify as Canon", ImVec2(-1, 0))) {
479 verify_target_id_ = entry.id;
480 show_verify_dialog_ = true;
481 }
482 }
483
484 if (entry.status != "deprecated") {
485 if (ImGui::Button(ICON_MD_DELETE " Deprecate", ImVec2(-1, 0))) {
486 deprecate_target_id_ = entry.id;
487 std::memset(deprecate_reason_, 0, sizeof(deprecate_reason_));
489 }
490 }
491
492 ImGui::Separator();
493
494 // Info
495 ImGui::Text("ID: %s", entry.id.c_str());
496 ImGui::Text("Path: %s", entry.path.c_str());
497
498 if (!entry.md5.empty()) {
499 ImGui::Text("MD5: %s", entry.md5.substr(0, 16).c_str());
500 }
501
502 if (!entry.location.empty()) {
503 ImGui::Text("Location: %s", entry.location.c_str());
504 }
505
506 if (entry.area > 0 || entry.link_x > 0) {
507 ImGui::Text("Area: 0x%02X Pos: (%d, %d)", entry.area, entry.link_x,
508 entry.link_y);
509 }
510
511 if (entry.health > 0) {
512 float ratio =
513 static_cast<float>(entry.health) / std::max(1, entry.max_health);
514 ImGui::Text("Health: %d/%d", entry.health, entry.max_health);
515 ImGui::ProgressBar(ratio, ImVec2(-1, 0));
516 }
517
518 if (!entry.captured_by.empty()) {
519 ImGui::Text("Captured by: %s", entry.captured_by.c_str());
520 }
521
522 if (!entry.verified_by.empty()) {
523 ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "Verified by: %s",
524 entry.verified_by.c_str());
525 if (!entry.verified_at.empty()) {
526 ImGui::TextDisabled("at %s", entry.verified_at.c_str());
527 }
528 }
529
530 if (!entry.deprecated_reason.empty()) {
531 ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "Deprecated: %s",
532 entry.deprecated_reason.c_str());
533 }
534
535 // Tags
536 if (!entry.tags.empty()) {
537 ImGui::Separator();
538 ImGui::Text("Tags:");
539 for (const auto& tag : entry.tags) {
540 ImGui::SameLine();
541 ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "[%s]", tag.c_str());
542 }
543 }
544}
545
547 // Verify dialog
549 ImGui::OpenPopup("Verify State");
550 show_verify_dialog_ = false;
551 }
552 if (ImGui::BeginPopupModal("Verify State", nullptr,
553 ImGuiWindowFlags_AlwaysAutoResize)) {
554 ImGui::Text("Promote '%s' to CANON status?", verify_target_id_.c_str());
555 ImGui::TextDisabled("This marks the state as verified and trusted.");
556
557 ImGui::InputText("Notes (optional)", verify_notes_, sizeof(verify_notes_));
558
559 if (ImGui::Button("Verify", ImVec2(120, 0))) {
560 auto status = VerifyState(verify_target_id_);
561 if (!status.ok()) {
562 status_message_ = std::string(status.message());
563 status_is_error_ = true;
564 }
565 std::memset(verify_notes_, 0, sizeof(verify_notes_));
566 ImGui::CloseCurrentPopup();
567 }
568 ImGui::SameLine();
569 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
570 ImGui::CloseCurrentPopup();
571 }
572 ImGui::EndPopup();
573 }
574
575 // Deprecate dialog
577 ImGui::OpenPopup("Deprecate State");
579 }
580 if (ImGui::BeginPopupModal("Deprecate State", nullptr,
581 ImGuiWindowFlags_AlwaysAutoResize)) {
582 ImGui::Text("Mark '%s' as DEPRECATED?", deprecate_target_id_.c_str());
583 ImGui::TextDisabled("This excludes the state from testing.");
584
585 ImGui::InputText("Reason", deprecate_reason_, sizeof(deprecate_reason_));
586
587 if (ImGui::Button("Deprecate", ImVec2(120, 0))) {
589 if (!status.ok()) {
590 status_message_ = std::string(status.message());
591 status_is_error_ = true;
592 }
593 std::memset(deprecate_reason_, 0, sizeof(deprecate_reason_));
594 ImGui::CloseCurrentPopup();
595 }
596 ImGui::SameLine();
597 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
598 ImGui::CloseCurrentPopup();
599 }
600 ImGui::EndPopup();
601 }
602}
603
604} // namespace editor
605} // namespace yaze
void SetClient(std::shared_ptr< emu::mesen::MesenSocketClient > client)
Set the Mesen socket client for emulator communication.
std::shared_ptr< emu::mesen::MesenSocketClient > client_
absl::Status VerifyState(const std::string &state_id)
Verify and promote a state to canon.
absl::Status DeprecateState(const std::string &state_id, const std::string &reason)
Deprecate a state.
absl::Status LoadState(const std::string &state_id)
Load a state into the emulator.
void RefreshLibrary()
Refresh the state library from disk.
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_DELETE
Definition icons.h:530
bool ItemContextMenu(const char *id, const std::function< std::vector< MenuItemSpec >()> &build, ImGuiPopupFlags flags)
Right-click menu for the last submitted ImGui item.
MenuItemSpec CopyToClipboardItem(const std::string &label, const std::string &text)
State entry from the Oracle save state library.
std::vector< std::string > tags
static CanvasMenuItem Destructive(const std::string &lbl, const std::string &ico, std::function< void()> cb, bool confirm=true, const std::string &sc="")