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