14#include "absl/strings/str_format.h"
15#include "absl/time/clock.h"
16#include "absl/time/time.h"
20#include "imgui/imgui.h"
21#include "nlohmann/json.hpp"
30 if (status ==
"canon") {
31 return ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
32 }
else if (status ==
"draft") {
33 return ImVec4(0.9f, 0.7f, 0.1f, 1.0f);
34 }
else if (status ==
"deprecated") {
35 return ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
37 return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
41 if (status ==
"canon")
43 if (status ==
"draft")
45 if (status ==
"deprecated")
54 const char* home = std::getenv(
"HOME");
57 "%s/src/hobby/oracle-of-secrets/Docs/Testing/save_state_library.json",
66 std::shared_ptr<emu::mesen::MesenSocketClient> client) {
80 if (!file.is_open()) {
87 nlohmann::json data = nlohmann::json::parse(file);
89 if (data.contains(
"library_root")) {
93 if (data.contains(
"entries") && data[
"entries"].is_array()) {
94 for (
const auto& entry_json : data[
"entries"]) {
96 entry.id = entry_json.value(
"id",
"");
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",
"");
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>());
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")) {
130 const auto& gs = entry_json[
"gameState"];
131 entry.indoors = gs.value(
"indoors",
false);
140 }
catch (
const std::exception& e) {
149 if (!file_in.is_open()) {
157 data = nlohmann::json::parse(file_in);
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;
176 if (!entry.deprecated_reason.empty()) {
177 entry_json[
"deprecated_reason"] = entry.deprecated_reason;
186 if (!file_out.is_open()) {
191 file_out << data.dump(2);
198 const StateEntry* entry =
nullptr;
200 if (ent.id == state_id) {
206 return absl::NotFoundError(
"State not found: " + state_id);
210 const char* home = std::getenv(
"HOME");
211 std::string socket_arg;
213 std::string path =
client_->GetSocketPath();
215 escaped.reserve(path.size() + 2);
217 for (
char c : path) {
218 if (c ==
'\\' || c ==
'"')
223 socket_arg =
" --socket " + escaped;
225 std::string cmd = absl::StrFormat(
226 "python3 %s/src/hobby/oracle-of-secrets/scripts/mesen2_client.py%s "
228 home ? home :
"", socket_arg, state_id.c_str());
230 FILE* pipe = popen(cmd.c_str(),
"r");
232 return absl::InternalError(
"Failed to execute lib-load command");
237 while (fgets(buffer,
sizeof(buffer), pipe)) {
240 int ret = pclose(pipe);
243 return absl::InternalError(
"lib-load failed: " + output);
248 return absl::OkStatus();
253 if (entry.id == state_id) {
254 entry.status =
"canon";
255 entry.verified_by =
"scawful";
256 entry.verified_at = absl::FormatTime(absl::RFC3339_full, absl::Now(),
257 absl::UTCTimeZone());
259 status_message_ = absl::StrFormat(
"Verified: %s", entry.label.c_str());
261 return absl::OkStatus();
264 return absl::NotFoundError(
"State not found: " + state_id);
268 const std::string& state_id,
const std::string& reason) {
270 if (entry.id == state_id) {
271 entry.status =
"deprecated";
272 entry.deprecated_reason = reason;
274 status_message_ = absl::StrFormat(
"Deprecated: %s", entry.label.c_str());
276 return absl::OkStatus();
279 return absl::NotFoundError(
"State not found: " + state_id);
283 ImGui::PushID(
"OracleStateLibraryPanel");
289 float details_width = 300.0f;
290 ImVec2 avail = ImGui::GetContentRegionAvail();
293 ImGui::BeginChild(
"StateList", ImVec2(avail.x - details_width - 10, 0),
true);
300 ImGui::BeginChild(
"StateDetails", ImVec2(details_width, 0),
true);
326 ImGui::SetNextItemWidth(150);
327 ImGui::InputTextWithHint(
"##filter",
"Filter...",
filter_text_,
339 int canon_count = 0, draft_count = 0, depr_count = 0;
341 if (e.status ==
"canon")
343 else if (e.status ==
"draft")
345 else if (e.status ==
"deprecated")
349 ImGui::TextDisabled(
"| %d canon, %d draft, %d deprecated", canon_count,
350 draft_count, depr_count);
355 std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
358 int visible_index = 0;
359 for (
size_t i = 0; i <
entries_.size(); ++i) {
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(),
378 if (label_lower.find(filter_lower) == std::string::npos &&
379 id_lower.find(filter_lower) == std::string::npos) {
391 if (ImGui::Selectable(entry.label.c_str(), is_selected,
392 ImGuiSelectableFlags_SpanAllColumns)) {
397 if (ImGui::BeginPopupContextItem()) {
405 if (entry.status ==
"draft") {
411 if (entry.status !=
"deprecated") {
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) {
427 ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f),
"[%s]",
428 entry.tags[t].c_str());
440 ImGui::TextDisabled(
"Select a state to view details");
450 ImGui::Text(
"%s", entry.label.c_str());
456 ImGui::TextDisabled(
"Connect to Mesen2 to load states");
467 if (entry.status ==
"draft") {
468 if (ImGui::Button(
ICON_MD_CHECK " Verify as Canon", ImVec2(-1, 0))) {
474 if (entry.status !=
"deprecated") {
485 ImGui::Text(
"ID: %s", entry.id.c_str());
486 ImGui::Text(
"Path: %s", entry.path.c_str());
488 if (!entry.md5.empty()) {
489 ImGui::Text(
"MD5: %s", entry.md5.substr(0, 16).c_str());
492 if (!entry.location.empty()) {
493 ImGui::Text(
"Location: %s", entry.location.c_str());
496 if (entry.area > 0 || entry.link_x > 0) {
497 ImGui::Text(
"Area: 0x%02X Pos: (%d, %d)", entry.area, entry.link_x,
501 if (entry.health > 0) {
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));
508 if (!entry.captured_by.empty()) {
509 ImGui::Text(
"Captured by: %s", entry.captured_by.c_str());
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());
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());
526 if (!entry.tags.empty()) {
528 ImGui::Text(
"Tags:");
529 for (
const auto& tag : entry.tags) {
531 ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f),
"[%s]", tag.c_str());
539 ImGui::OpenPopup(
"Verify State");
542 if (ImGui::BeginPopupModal(
"Verify State",
nullptr,
543 ImGuiWindowFlags_AlwaysAutoResize)) {
545 ImGui::TextDisabled(
"This marks the state as verified and trusted.");
549 if (ImGui::Button(
"Verify", ImVec2(120, 0))) {
556 ImGui::CloseCurrentPopup();
559 if (ImGui::Button(
"Cancel", ImVec2(120, 0))) {
560 ImGui::CloseCurrentPopup();
567 ImGui::OpenPopup(
"Deprecate State");
570 if (ImGui::BeginPopupModal(
"Deprecate State",
nullptr,
571 ImGuiWindowFlags_AlwaysAutoResize)) {
573 ImGui::TextDisabled(
"This excludes the state from testing.");
577 if (ImGui::Button(
"Deprecate", ImVec2(120, 0))) {
584 ImGui::CloseCurrentPopup();
587 if (ImGui::Button(
"Cancel", ImVec2(120, 0))) {
588 ImGui::CloseCurrentPopup();
std::string manifest_path_
void Draw()
Draw the panel.
void SetClient(std::shared_ptr< emu::mesen::MesenSocketClient > client)
Set the Mesen socket client for emulator communication.
char deprecate_reason_[256]
std::string verify_target_id_
std::shared_ptr< emu::mesen::MesenSocketClient > client_
bool show_deprecate_dialog_
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.
std::string library_root_
~OracleStateLibraryPanel()
std::vector< StateEntry > entries_
void DrawVerificationDialog()
std::string deprecate_target_id_
std::string status_message_
OracleStateLibraryPanel()
#define ICON_MD_PLAY_ARROW
std::string GetStatusBadge(const std::string &status)
ImVec4 GetStatusColor(const std::string &status)