yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
story_event_graph_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_ORACLE_PANELS_STORY_EVENT_GRAPH_PANEL_H
2#define YAZE_APP_EDITOR_ORACLE_PANELS_STORY_EVENT_GRAPH_PANEL_H
3
4#include <atomic>
5#include <cmath>
6#include <cstdint>
7#include <filesystem>
8#include <memory>
9#include <optional>
10#include <string>
11#include <unordered_map>
12#include <vector>
13
19#include "app/gui/core/icons.h"
20#include "core/hack_manifest.h"
22#include "core/project.h"
25#include "imgui/imgui.h"
26#include "imgui/misc/cpp/imgui_stdlib.h"
27#include "util/file_util.h"
28
29namespace yaze::editor {
30
42class StoryEventGraphPanel : public WindowContent {
43 public:
46
50 void SetManifest(core::HackManifest* manifest) { manifest_ = manifest; }
51
52 std::string GetId() const override { return "oracle.story_event_graph"; }
53 std::string GetDisplayName() const override { return "Story Event Graph"; }
54 std::string GetIcon() const override { return ICON_MD_ACCOUNT_TREE; }
55 std::string GetEditorCategory() const override { return "Agent"; }
56 std::string GetWorkflowGroup() const override { return "Planning"; }
57 std::string GetWorkflowDescription() const override {
58 return "Inspect narrative and progression dependencies for the active project";
59 }
60 bool IsEnabled() const override {
62 auto* backend = GetPlanningBackend();
63 return project != nullptr && project->project_opened() && backend != nullptr;
64 }
65 std::string GetDisabledTooltip() const override {
66 return "Story graph data is not available for the active hack project";
67 }
71 float GetPreferredWidth() const override { return 600.0f; }
72
73 void Draw(bool* /*p_open*/) override {
74 if (!IsEnabled()) {
75 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
76 return;
77 }
78
79 // Lazily resolve the manifest from the project context
80 if (!manifest_) {
82 if (auto* backend = GetWorkflowBackend()) {
83 manifest_ = backend->ResolveManifest(project);
84 } else if (project && project->hack_manifest.loaded()) {
85 manifest_ = &project->hack_manifest;
86 }
87 }
88
90 ImGui::TextDisabled("No hack project loaded");
91 ImGui::TextDisabled(
92 "Open a project with a hack manifest to view story events.");
93 return;
94 }
95
99
100 const auto* graph = GetStoryGraph();
101 if (graph == nullptr || !graph->loaded()) {
102 ImGui::TextDisabled("No story events data available");
103 return;
104 }
105
106 // Controls row
107 if (ImGui::Button("Reset View")) {
108 scroll_x_ = 0;
109 scroll_y_ = 0;
110 zoom_ = 1.0f;
111 }
112 ImGui::SameLine();
113 ImGui::SliderFloat("Zoom", &zoom_, 0.3f, 2.0f, "%.1f");
114 ImGui::SameLine();
115 ImGui::Text("Nodes: %zu Edges: %zu", graph->nodes().size(),
116 graph->edges().size());
117 ImGui::SameLine();
118 const auto prog_opt = GetProgressionBackend()
121 if (prog_opt.has_value()) {
122 ImGui::TextDisabled("Crystals: %d State: %s",
123 prog_opt->GetCrystalCount(),
124 prog_opt->GetGameStateName().c_str());
125 } else {
126 ImGui::TextDisabled("No SRAM loaded");
127 }
128
129 ImGui::SameLine();
130 if (ImGui::SmallButton("Import .srm...")) {
132 }
133 ImGui::SameLine();
134 if (ImGui::SmallButton("Clear SRAM")) {
136 }
137 ImGui::SameLine();
139 if (!loaded_srm_path_.empty()) {
140 const std::filesystem::path p(loaded_srm_path_);
141 ImGui::SameLine();
142 ImGui::TextDisabled("SRM: %s", p.filename().string().c_str());
143 if (ImGui::IsItemHovered()) {
144 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
145 }
146 }
147
148 if (!last_srm_error_.empty()) {
149 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "SRM error: %s",
150 last_srm_error_.c_str());
151 }
152
153 ImGui::Separator();
154
155 DrawFilterControls(*graph);
156 UpdateFilterCache(*graph);
157
158 ImGui::Separator();
159
160 // Main canvas area
161 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
162 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
163
164 // Reserve space for detail sidebar if a node is selected
165 float sidebar_width = selected_node_.empty() ? 0.0f : 250.0f;
166 canvas_size.x -= sidebar_width;
167
168 if (canvas_size.x < 100 || canvas_size.y < 100) return;
169
170 ImGui::InvisibleButton("story_canvas", canvas_size,
171 ImGuiButtonFlags_MouseButtonLeft |
172 ImGuiButtonFlags_MouseButtonRight);
173
174 bool is_hovered = ImGui::IsItemHovered();
175 bool is_active = ImGui::IsItemActive();
176
177 // Pan with right mouse button
178 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
179 ImVec2 delta = ImGui::GetIO().MouseDelta;
180 scroll_x_ += delta.x;
181 scroll_y_ += delta.y;
182 }
183
184 // Zoom with scroll wheel
185 if (is_hovered) {
186 float wheel = ImGui::GetIO().MouseWheel;
187 if (wheel != 0.0f) {
188 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
189 if (zoom_ < 0.3f) zoom_ = 0.3f;
190 if (zoom_ > 2.0f) zoom_ = 2.0f;
191 }
192 }
193
194 ImDrawList* draw_list = ImGui::GetWindowDrawList();
195
196 // Clip to canvas
197 draw_list->PushClipRect(canvas_pos,
198 ImVec2(canvas_pos.x + canvas_size.x,
199 canvas_pos.y + canvas_size.y),
200 true);
201
202 // Center offset
203 float cx = canvas_pos.x + canvas_size.x * 0.5f + scroll_x_;
204 float cy = canvas_pos.y + canvas_size.y * 0.5f + scroll_y_;
205
206 // Draw edges first (behind nodes)
207 for (const auto& edge : graph->edges()) {
208 const auto* from_node = graph->GetNode(edge.from);
209 const auto* to_node = graph->GetNode(edge.to);
210 if (!from_node || !to_node) continue;
211 if (hide_non_matching_) {
212 if (!IsNodeVisible(edge.from) || !IsNodeVisible(edge.to)) continue;
213 }
214
215 ImVec2 p1(cx + from_node->pos_x * zoom_ + kNodeWidth * zoom_ * 0.5f,
216 cy + from_node->pos_y * zoom_);
217 ImVec2 p2(cx + to_node->pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f,
218 cy + to_node->pos_y * zoom_);
219
220 // Bezier control points
221 float ctrl_dx = (p2.x - p1.x) * 0.4f;
222 ImVec2 cp1(p1.x + ctrl_dx, p1.y);
223 ImVec2 cp2(p2.x - ctrl_dx, p2.y);
224
225 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
226 1.5f * zoom_);
227
228 // Arrow head
229 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
230 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
231 if (len > 0) {
232 dir.x /= len;
233 dir.y /= len;
234 float arrow_size = 8.0f * zoom_;
235 ImVec2 arrow1(p2.x - dir.x * arrow_size + dir.y * arrow_size * 0.4f,
236 p2.y - dir.y * arrow_size - dir.x * arrow_size * 0.4f);
237 ImVec2 arrow2(p2.x - dir.x * arrow_size - dir.y * arrow_size * 0.4f,
238 p2.y - dir.y * arrow_size + dir.x * arrow_size * 0.4f);
239 draw_list->AddTriangleFilled(p2, arrow1, arrow2,
240 IM_COL32(150, 150, 150, 200));
241 }
242 }
243
244 // Draw nodes
245 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
246
247 for (const auto& node : graph->nodes()) {
248 if (hide_non_matching_ && !IsNodeVisible(node.id)) {
249 continue;
250 }
251
252 float nx = cx + node.pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f;
253 float ny = cy + node.pos_y * zoom_ - kNodeHeight * zoom_ * 0.5f;
254 float nw = kNodeWidth * zoom_;
255 float nh = kNodeHeight * zoom_;
256
257 ImVec2 node_min(nx, ny);
258 ImVec2 node_max(nx + nw, ny + nh);
259
260 // Color by status
261 ImU32 fill_color = GetStatusColor(node.status);
262 const bool selected = (node.id == selected_node_);
263 const bool query_match = (HasNonEmptyQuery() && IsNodeQueryMatch(node.id));
264 ImU32 border_color = selected ? IM_COL32(255, 255, 100, 255)
265 : (query_match ? IM_COL32(220, 220, 220, 255)
266 : IM_COL32(60, 60, 60, 255));
267
268 draw_list->AddRectFilled(node_min, node_max, fill_color, 8.0f * zoom_);
269 draw_list->AddRect(node_min, node_max, border_color, 8.0f * zoom_,
270 0, 2.0f * zoom_);
271
272 // Node text
273 float font_size = 11.0f * zoom_;
274 if (font_size >= 6.0f) {
275 // ID label
276 draw_list->AddText(nullptr, font_size,
277 ImVec2(nx + 6 * zoom_, ny + 4 * zoom_),
278 IM_COL32(200, 200, 200, 255),
279 node.id.c_str());
280 // Name (truncated)
281 std::string display_name = node.name;
282 if (display_name.length() > 25) {
283 display_name = display_name.substr(0, 22) + "...";
284 }
285 draw_list->AddText(nullptr, font_size,
286 ImVec2(nx + 6 * zoom_, ny + 18 * zoom_),
287 IM_COL32(255, 255, 255, 255),
288 display_name.c_str());
289 }
290
291 // Click detection
292 if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
293 if (mouse_pos.x >= node_min.x && mouse_pos.x <= node_max.x &&
294 mouse_pos.y >= node_min.y && mouse_pos.y <= node_max.y) {
295 selected_node_ = (selected_node_ == node.id) ? "" : node.id;
296 }
297 }
298 }
299
300 draw_list->PopClipRect();
301
302 // Detail sidebar
303 if (!selected_node_.empty() && sidebar_width > 0) {
304 ImGui::SameLine();
305 ImGui::BeginGroup();
306 DrawNodeDetail(*graph);
307 ImGui::EndGroup();
308 }
309 }
310
311 private:
312 static constexpr float kNodeWidth = 160.0f;
313 static constexpr float kNodeHeight = 40.0f;
314
316 switch (status) {
318 return IM_COL32(40, 120, 40, 220);
320 return IM_COL32(180, 160, 40, 220);
322 return IM_COL32(160, 40, 40, 220);
324 default:
325 return IM_COL32(60, 60, 60, 220);
326 }
327 }
328
330 const auto* node = graph.GetNode(selected_node_);
331 if (!node) return;
332
333 ImGui::BeginChild("node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
334
335 ImGui::TextWrapped("%s", node->name.c_str());
336 ImGui::TextDisabled("%s", node->id.c_str());
337 ImGui::Separator();
338
339 if (!node->flags.empty()) {
340 ImGui::Text("Flags:");
341 for (const auto& flag : node->flags) {
342 if (!flag.value.empty()) {
343 ImGui::BulletText("%s = %s", flag.name.c_str(), flag.value.c_str());
344 } else {
345 ImGui::BulletText("%s", flag.name.c_str());
346 }
347 }
348 }
349
350 if (!node->locations.empty()) {
351 ImGui::Spacing();
352 ImGui::Text("Locations:");
353 for (size_t i = 0; i < node->locations.size(); ++i) {
354 const auto& loc = node->locations[i];
355 ImGui::PushID(static_cast<int>(i));
356
357 ImGui::BulletText("%s", loc.name.c_str());
358
359 if (auto room_id = ParseIntLoose(loc.room_id)) {
360 ImGui::SameLine();
361 if (ImGui::SmallButton("Room")) {
362 PublishJumpToRoom(*room_id);
363 }
364 }
365 if (auto map_id = ParseIntLoose(loc.overworld_id)) {
366 ImGui::SameLine();
367 if (ImGui::SmallButton("Map")) {
368 PublishJumpToMap(*map_id);
369 }
370 }
371
372 if (!loc.room_id.empty() || !loc.overworld_id.empty() ||
373 !loc.entrance_id.empty()) {
374 ImGui::TextDisabled("room=%s map=%s entrance=%s",
375 loc.room_id.empty() ? "-" : loc.room_id.c_str(),
376 loc.overworld_id.empty() ? "-" : loc.overworld_id.c_str(),
377 loc.entrance_id.empty() ? "-" : loc.entrance_id.c_str());
378 }
379
380 ImGui::PopID();
381 }
382 }
383
384 if (!node->text_ids.empty()) {
385 ImGui::Spacing();
386 ImGui::Text("Text IDs:");
387 for (size_t i = 0; i < node->text_ids.size(); ++i) {
388 const auto& tid = node->text_ids[i];
389 ImGui::PushID(static_cast<int>(i));
390
391 ImGui::BulletText("%s", tid.c_str());
392 ImGui::SameLine();
393 if (ImGui::SmallButton("Open")) {
394 if (auto msg_id = ParseIntLoose(tid)) {
395 PublishJumpToMessage(*msg_id);
396 }
397 }
398 ImGui::SameLine();
399 if (ImGui::SmallButton("Copy")) {
400 ImGui::SetClipboardText(tid.c_str());
401 }
402
403 ImGui::PopID();
404 }
405 }
406
407 if (!node->scripts.empty()) {
408 ImGui::Spacing();
409 ImGui::Text("Scripts:");
410 for (size_t i = 0; i < node->scripts.size(); ++i) {
411 const auto& script = node->scripts[i];
412 ImGui::PushID(static_cast<int>(i));
413 ImGui::BulletText("%s", script.c_str());
414 ImGui::SameLine();
415 if (ImGui::SmallButton("Open")) {
417 }
418 ImGui::SameLine();
419 if (ImGui::SmallButton("Copy")) {
420 ImGui::SetClipboardText(script.c_str());
421 }
422 ImGui::PopID();
423 }
424 }
425
426 if (!node->notes.empty()) {
427 ImGui::Spacing();
428 ImGui::TextWrapped("Notes: %s", node->notes.c_str());
429 }
430
431 ImGui::EndChild();
432 }
433
434 static std::optional<int> ParseIntLoose(const std::string& input) {
435 // Trim whitespace.
436 size_t start = input.find_first_not_of(" \t\r\n");
437 if (start == std::string::npos) return std::nullopt;
438 size_t end = input.find_last_not_of(" \t\r\n");
439 std::string trimmed = input.substr(start, end - start + 1);
440
441 try {
442 size_t idx = 0;
443 int value = std::stoi(trimmed, &idx, /*base=*/0);
444 if (idx != trimmed.size()) return std::nullopt;
445 return value;
446 } catch (...) {
447 return std::nullopt;
448 }
449 }
450
451 void PublishJumpToRoom(int room_id) const {
452 if (auto* bus = ContentRegistry::Context::event_bus()) {
453 bus->Publish(JumpToRoomRequestEvent::Create(room_id));
454 }
455 }
456
457 void PublishJumpToMap(int map_id) const {
458 if (auto* bus = ContentRegistry::Context::event_bus()) {
459 bus->Publish(JumpToMapRequestEvent::Create(map_id));
460 }
461 }
462
463 void PublishJumpToMessage(int message_id) const {
464 if (auto* bus = ContentRegistry::Context::event_bus()) {
465 bus->Publish(JumpToMessageRequestEvent::Create(message_id));
466 }
467 }
468
469 void PublishJumpToAssemblySymbol(const std::string& symbol) const {
470 if (auto* bus = ContentRegistry::Context::event_bus()) {
471 bus->Publish(JumpToAssemblySymbolRequestEvent::Create(symbol));
472 }
473 }
474
476 if (!manifest_) return 0;
477 const auto prog_opt = GetProgressionBackend()
480 if (!prog_opt.has_value()) return 0;
481
482 const auto& s = *prog_opt;
483 return static_cast<uint64_t>(s.crystal_bitfield) |
484 (static_cast<uint64_t>(s.game_state) << 8) |
485 (static_cast<uint64_t>(s.oosprog) << 16) |
486 (static_cast<uint64_t>(s.oosprog2) << 24) |
487 (static_cast<uint64_t>(s.side_quest) << 32) |
488 (static_cast<uint64_t>(s.pendants) << 40);
489 }
490
492 (void)graph;
493
494 ImGui::Text("Filter");
495 ImGui::SameLine();
496 ImGui::SetNextItemWidth(260.0f);
497 if (ImGui::InputTextWithHint("##story_graph_filter",
498 "Search id/name/text/script/flag/room...",
499 &filter_query_)) {
500 filter_dirty_ = true;
501 }
502 ImGui::SameLine();
503 if (ImGui::SmallButton("Clear")) {
504 if (!filter_query_.empty()) {
505 filter_query_.clear();
506 filter_dirty_ = true;
507 }
508 }
509
510 ImGui::SameLine();
511 if (ImGui::Checkbox("Hide non-matching", &hide_non_matching_)) {
512 // Hiding doesn't change matches, but it can invalidate selection.
513 filter_dirty_ = true;
514 }
515
516 ImGui::SameLine();
517 bool toggles_changed = false;
518 toggles_changed |= ImGui::Checkbox("Completed", &show_completed_);
519 ImGui::SameLine();
520 toggles_changed |= ImGui::Checkbox("Available", &show_available_);
521 ImGui::SameLine();
522 toggles_changed |= ImGui::Checkbox("Locked", &show_locked_);
523 ImGui::SameLine();
524 toggles_changed |= ImGui::Checkbox("Blocked", &show_blocked_);
525 if (toggles_changed) {
526 filter_dirty_ = true;
527 }
528 }
529
530 static uint8_t StatusMask(bool completed, bool available, bool locked,
531 bool blocked) {
532 uint8_t mask = 0;
533 if (completed) mask |= 1u << 0;
534 if (available) mask |= 1u << 1;
535 if (locked) mask |= 1u << 2;
536 if (blocked) mask |= 1u << 3;
537 return mask;
538 }
539
540 bool HasNonEmptyQuery() const { return !filter_query_.empty(); }
541
542 bool IsNodeVisible(const std::string& id) const {
543 auto it = node_visible_by_id_.find(id);
544 return it != node_visible_by_id_.end() ? it->second : true;
545 }
546
547 bool IsNodeQueryMatch(const std::string& id) const {
548 auto it = node_query_match_by_id_.find(id);
549 return it != node_query_match_by_id_.end() ? it->second : false;
550 }
551
553 const uint8_t status_mask =
555 const uint64_t progress_fp = ComputeProgressionFingerprint();
556
557 const size_t node_count = graph.nodes().size();
558 if (!filter_dirty_ && node_count == last_node_count_ &&
560 progress_fp == last_progress_fp_) {
561 return;
562 }
563
564 last_node_count_ = node_count;
566 last_status_mask_ = status_mask;
567 last_progress_fp_ = progress_fp;
568 filter_dirty_ = false;
569
571 filter.query = filter_query_;
576
578 node_visible_by_id_.clear();
579 node_query_match_by_id_.reserve(node_count);
580 node_visible_by_id_.reserve(node_count);
581
582 for (const auto& node : graph.nodes()) {
583 const bool query_match = core::StoryEventNodeMatchesQuery(node, filter.query);
584 const bool visible =
585 query_match && core::StoryNodeStatusAllowed(node.status, filter);
586 node_query_match_by_id_[node.id] = query_match;
587 node_visible_by_id_[node.id] = visible;
588 }
589
590 // If we're hiding nodes and the selection becomes invisible, clear it to
591 // avoid a "ghost sidebar" pointing at a filtered-out node.
592 if (hide_non_matching_ && !selected_node_.empty() &&
594 selected_node_.clear();
595 }
596 }
597
599 if (!manifest_) return;
600
602 options.filters = {
603 {"SRAM (.srm)", "srm"},
604 {"All Files", "*"},
605 };
606
607 std::string file_path =
609 if (file_path.empty()) {
610 return;
611 }
612
613 auto state_or = GetProgressionBackend()
615 file_path)
617 if (!state_or.ok()) {
618 last_srm_error_ = std::string(state_or.status().message());
619 return;
620 }
621
622 if (auto* backend = GetProgressionBackend()) {
623 backend->SetProgressionState(*manifest_, *state_or);
624 } else {
626 }
627 loaded_srm_path_ = file_path;
628 last_srm_error_.clear();
629
630 // Status coloring changed; refresh filter cache visibility.
631 filter_dirty_ = true;
632 }
633
635 if (!manifest_) return;
636 if (auto* backend = GetProgressionBackend()) {
637 backend->ClearProgressionState(*manifest_);
638 } else {
640 }
641 loaded_srm_path_.clear();
642 last_srm_error_.clear();
643 filter_dirty_ = true;
644 }
645
647 const bool connected = live_client_ && live_client_->IsConnected();
648 if (ImGui::SmallButton("Sync Mesen")) {
649 live_refresh_pending_.store(false);
651 }
652 if (ImGui::IsItemHovered()) {
653 ImGui::SetTooltip("Read Oracle SRAM directly from connected Mesen2");
654 }
655
656 ImGui::SameLine();
657 ImGui::Checkbox("Live", &live_sync_enabled_);
658 if (live_sync_enabled_) {
659 ImGui::SameLine();
660 ImGui::SetNextItemWidth(70.0f);
661 ImGui::SliderFloat("##StoryGraphLiveInterval", &live_refresh_interval_seconds_,
662 0.05f, 0.5f, "%.2fs");
663 }
664
665 ImGui::SameLine();
666 if (connected) {
667 ImGui::TextDisabled("Mesen: connected");
668 } else {
669 ImGui::TextDisabled("Mesen: disconnected");
670 }
671
672 if (!live_sync_error_.empty()) {
673 ImGui::SameLine();
674 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), "Live sync error");
675 if (ImGui::IsItemHovered()) {
676 ImGui::SetTooltip("%s", live_sync_error_.c_str());
677 }
678 }
679 }
680
683 if (client == live_client_) {
684 return;
685 }
686
688 live_client_ = std::move(client);
690 live_sync_error_.clear();
691
692 if (live_client_ && live_client_->IsConnected()) {
693 live_refresh_pending_.store(true);
694 }
695 }
696
698 if (!live_sync_enabled_) {
699 return;
700 }
701 if (!live_client_ || !live_client_->IsConnected()) {
703 return;
704 }
705
706 if (live_listener_id_ == 0) {
708 live_client_->AddEventListener([this](const emu::mesen::MesenEvent& event) {
709 if (event.type == "frame_complete" ||
710 event.type == "breakpoint_hit" || event.type == "all") {
711 live_refresh_pending_.store(true);
712 }
713 });
714 }
715
717 return;
718 }
719
720 const double now = ImGui::GetTime();
721 if ((now - last_subscribe_attempt_time_) < 1.0) {
722 return;
723 }
725
726 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
727 if (!status.ok()) {
728 live_sync_error_ = std::string(status.message());
729 return;
730 }
731
733 live_sync_error_.clear();
734 live_refresh_pending_.store(true);
735 }
736
738 if (!live_sync_enabled_) {
739 return;
740 }
741 if (!live_refresh_pending_.load()) {
742 return;
743 }
744 const double now = ImGui::GetTime();
746 return;
747 }
750 }
751 live_refresh_pending_.store(false);
752 }
753
755 if (!manifest_) {
756 return false;
757 }
758 if (!live_client_ || !live_client_->IsConnected()) {
759 live_sync_error_ = "Mesen client is not connected";
760 return false;
761 }
762
763 auto* backend = GetProgressionBackend();
764 if (backend == nullptr) {
765 live_sync_error_ = "No hack workflow backend is available";
766 return false;
767 }
768
769 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
770 if (!state_or.ok()) {
771 live_sync_error_ = std::string(state_or.status().message());
772 return false;
773 }
774
775 backend->SetProgressionState(*manifest_, *state_or);
776 loaded_srm_path_ = "Mesen2 Live";
777 last_srm_error_.clear();
778 live_sync_error_.clear();
779 filter_dirty_ = true;
780 return true;
781 }
782
784 if (live_client_ && live_listener_id_ != 0) {
785 live_client_->RemoveEventListener(live_listener_id_);
786 }
789 }
790
794
798
802
804 if (!manifest_) {
805 return nullptr;
806 }
807 if (auto* backend = GetPlanningBackend()) {
808 return backend->GetStoryGraph(*manifest_);
809 }
811 }
812
813 core::HackManifest* manifest_ = nullptr;
814 std::string selected_node_;
815 float scroll_x_ = 0;
816 float scroll_y_ = 0;
817 float zoom_ = 1.0f;
818
819 // Filter state
820 std::string filter_query_;
821 bool hide_non_matching_ = false;
822 bool show_completed_ = true;
823 bool show_available_ = true;
824 bool show_locked_ = true;
825 bool show_blocked_ = true;
826
827 // Filter cache (recomputed only when query/toggles change)
828 bool filter_dirty_ = true;
829 size_t last_node_count_ = 0;
830 std::string last_filter_query_;
831 uint8_t last_status_mask_ = 0;
832 std::unordered_map<std::string, bool> node_query_match_by_id_;
833 std::unordered_map<std::string, bool> node_visible_by_id_;
834
835 // SRAM import state (purely UI; the actual progression state lives in HackManifest).
836 std::string loaded_srm_path_;
837 std::string last_srm_error_;
838
839 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
841 bool live_sync_enabled_ = true;
842 bool live_subscription_active_ = false;
843 std::atomic<bool> live_refresh_pending_{false};
844 float live_refresh_interval_seconds_ = 0.10f;
845 double last_live_refresh_time_ = 0.0;
846 double last_subscribe_attempt_time_ = 0.0;
847 std::string live_sync_error_;
848
849 uint64_t last_progress_fp_ = 0;
850};
851
852} // namespace yaze::editor
853
854#endif // YAZE_APP_EDITOR_ORACLE_PANELS_STORY_EVENT_GRAPH_PANEL_H
Loads and queries the hack manifest JSON for yaze-ASM integration.
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
std::optional< OracleProgressionState > oracle_progression_state() const
void SetOracleProgressionState(const OracleProgressionState &state)
The complete Oracle narrative progression graph.
const std::vector< StoryEventNode > & nodes() const
const StoryEventNode * GetNode(const std::string &id) const
workflow::HackWorkflowBackend * GetWorkflowBackend() const
WindowLifecycle GetWindowLifecycle() const override
Get the lifecycle category for this window.
bool IsNodeQueryMatch(const std::string &id) const
const core::StoryEventGraph * GetStoryGraph() const
std::unordered_map< std::string, bool > node_visible_by_id_
bool IsNodeVisible(const std::string &id) const
std::string GetId() const override
Unique identifier for this panel.
void DrawFilterControls(const core::StoryEventGraph &graph)
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
void SetManifest(core::HackManifest *manifest)
Inject manifest pointer (called by host editor or lazy-resolved).
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void PublishJumpToAssemblySymbol(const std::string &symbol) const
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
workflow::PlanningCapability * GetPlanningBackend() const
void PublishJumpToMessage(int message_id) const
emu::mesen::EventListenerId live_listener_id_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
std::shared_ptr< emu::mesen::MesenSocketClient > live_client_
workflow::ProgressionCapability * GetProgressionBackend() const
void DrawNodeDetail(const core::StoryEventGraph &graph)
void Draw(bool *) override
Draw the panel content.
static ImU32 GetStatusColor(core::StoryNodeStatus status)
static uint8_t StatusMask(bool completed, bool available, bool locked, bool blocked)
bool IsEnabled() const override
Check if this panel is currently enabled.
static std::optional< int > ParseIntLoose(const std::string &input)
void UpdateFilterCache(const core::StoryEventGraph &graph)
std::unordered_map< std::string, bool > node_query_match_by_id_
std::string GetWorkflowDescription() const override
Optional workflow description for menus/command palette.
std::string GetDisabledTooltip() const override
Get tooltip text when panel is disabled.
virtual std::optional< core::OracleProgressionState > GetProgressionState(const core::HackManifest &manifest) const =0
virtual absl::StatusOr< core::OracleProgressionState > LoadProgressionStateFromFile(const std::string &filepath) const =0
static std::shared_ptr< MesenSocketClient > & GetClient()
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define ICON_MD_ACCOUNT_TREE
Definition icons.h:83
absl::StatusOr< OracleProgressionState > LoadOracleProgressionFromSrmFile(const std::string &srm_path)
bool StoryEventNodeMatchesQuery(const StoryEventNode &node, std::string_view query)
bool StoryNodeStatusAllowed(StoryNodeStatus status, const StoryEventNodeFilter &filter)
StoryNodeStatus
Completion status of a story event node for rendering.
::yaze::EventBus * event_bus()
Get the current EventBus instance.
workflow::PlanningCapability * hack_planning_backend()
::yaze::project::YazeProject * current_project()
Get the current project instance.
workflow::HackWorkflowBackend * hack_workflow_backend()
workflow::ProgressionCapability * hack_progression_backend()
Editors are the view controllers for the application.
WindowLifecycle
Defines lifecycle behavior for editor windows.
@ CrossEditor
User can pin to persist across editors.
Filter options for StoryEventGraph node search in UI.
static JumpToAssemblySymbolRequestEvent Create(std::string sym, size_t session=0)
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToMessageRequestEvent Create(int message, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
Event from Mesen2 subscription.
std::vector< FileDialogFilter > filters
Definition file_util.h:17