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_HACK_ORACLE_UI_STORY_EVENT_GRAPH_PANEL_H
2#define YAZE_APP_EDITOR_HACK_ORACLE_UI_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
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 "
59 "project";
60 }
61 bool IsEnabled() const override {
63 auto* backend = GetPlanningBackend();
64 return project != nullptr && project->project_opened() &&
65 backend != nullptr;
66 }
67 std::string GetDisabledTooltip() const override {
68 return "Story graph data is not available for the active hack project";
69 }
73 float GetPreferredWidth() const override { return 600.0f; }
74
75 void Draw(bool* /*p_open*/) override {
76 if (!IsEnabled()) {
77 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
78 return;
79 }
80
81 // Lazily resolve the manifest from the project context
82 if (!manifest_) {
84 if (auto* backend = GetWorkflowBackend()) {
85 manifest_ = backend->ResolveManifest(project);
86 } else if (project && project->hack_manifest.loaded()) {
87 manifest_ = &project->hack_manifest;
88 }
89 }
90
92 ImGui::TextDisabled("No hack project loaded");
93 ImGui::TextDisabled(
94 "Open a project with a hack manifest to view story events.");
95 return;
96 }
97
101
102 const auto* graph = GetStoryGraph();
103 if (graph == nullptr || !graph->loaded()) {
104 ImGui::TextDisabled("No story events data available");
105 return;
106 }
107
108 // Controls row
109 if (ImGui::Button("Reset View")) {
110 scroll_x_ = 0;
111 scroll_y_ = 0;
112 zoom_ = 1.0f;
113 }
114 ImGui::SameLine();
115 ImGui::SliderFloat("Zoom", &zoom_, 0.3f, 2.0f, "%.1f");
116 ImGui::SameLine();
117 ImGui::Text("Nodes: %zu Edges: %zu", graph->nodes().size(),
118 graph->edges().size());
119 ImGui::SameLine();
120 const auto prog_opt =
124 if (prog_opt.has_value()) {
125 ImGui::TextDisabled("Crystals: %d State: %s",
126 prog_opt->GetCrystalCount(),
127 prog_opt->GetGameStateName().c_str());
128 } else {
129 ImGui::TextDisabled("No SRAM loaded");
130 }
131
132 ImGui::SameLine();
133 if (ImGui::SmallButton("Import .srm...")) {
135 }
136 ImGui::SameLine();
137 if (ImGui::SmallButton("Clear SRAM")) {
139 }
140 ImGui::SameLine();
142 if (!loaded_srm_path_.empty()) {
143 const std::filesystem::path p(loaded_srm_path_);
144 ImGui::SameLine();
145 ImGui::TextDisabled("SRM: %s", p.filename().string().c_str());
146 if (ImGui::IsItemHovered()) {
147 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
148 }
149 }
150
151 if (!last_srm_error_.empty()) {
152 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "SRM error: %s",
153 last_srm_error_.c_str());
154 }
155
156 ImGui::Separator();
157
158 DrawFilterControls(*graph);
159 UpdateFilterCache(*graph);
160
161 ImGui::Separator();
162
163 // Main canvas area
164 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
165 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
166
167 // Reserve space for detail sidebar if a node is selected
168 float sidebar_width = selected_node_.empty() ? 0.0f : 250.0f;
169 canvas_size.x -= sidebar_width;
170
171 if (canvas_size.x < 100 || canvas_size.y < 100)
172 return;
173
174 ImGui::InvisibleButton(
175 "story_canvas", canvas_size,
176 ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight);
177
178 bool is_hovered = ImGui::IsItemHovered();
179 bool is_active = ImGui::IsItemActive();
180
181 // Pan with right mouse button
182 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
183 ImVec2 delta = ImGui::GetIO().MouseDelta;
184 scroll_x_ += delta.x;
185 scroll_y_ += delta.y;
186 }
187
188 // Zoom with scroll wheel
189 if (is_hovered) {
190 float wheel = ImGui::GetIO().MouseWheel;
191 if (wheel != 0.0f) {
192 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
193 if (zoom_ < 0.3f)
194 zoom_ = 0.3f;
195 if (zoom_ > 2.0f)
196 zoom_ = 2.0f;
197 }
198 }
199
200 ImDrawList* draw_list = ImGui::GetWindowDrawList();
201
202 // Clip to canvas
203 draw_list->PushClipRect(
204 canvas_pos,
205 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
206 true);
207
208 // Center offset
209 float cx = canvas_pos.x + canvas_size.x * 0.5f + scroll_x_;
210 float cy = canvas_pos.y + canvas_size.y * 0.5f + scroll_y_;
211
212 // Draw edges first (behind nodes)
213 for (const auto& edge : graph->edges()) {
214 const auto* from_node = graph->GetNode(edge.from);
215 const auto* to_node = graph->GetNode(edge.to);
216 if (!from_node || !to_node)
217 continue;
218 if (hide_non_matching_) {
219 if (!IsNodeVisible(edge.from) || !IsNodeVisible(edge.to))
220 continue;
221 }
222
223 ImVec2 p1(cx + from_node->pos_x * zoom_ + kNodeWidth * zoom_ * 0.5f,
224 cy + from_node->pos_y * zoom_);
225 ImVec2 p2(cx + to_node->pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f,
226 cy + to_node->pos_y * zoom_);
227
228 // Bezier control points
229 float ctrl_dx = (p2.x - p1.x) * 0.4f;
230 ImVec2 cp1(p1.x + ctrl_dx, p1.y);
231 ImVec2 cp2(p2.x - ctrl_dx, p2.y);
232
233 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
234 1.5f * zoom_);
235
236 // Arrow head
237 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
238 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
239 if (len > 0) {
240 dir.x /= len;
241 dir.y /= len;
242 float arrow_size = 8.0f * zoom_;
243 ImVec2 arrow1(p2.x - dir.x * arrow_size + dir.y * arrow_size * 0.4f,
244 p2.y - dir.y * arrow_size - dir.x * arrow_size * 0.4f);
245 ImVec2 arrow2(p2.x - dir.x * arrow_size - dir.y * arrow_size * 0.4f,
246 p2.y - dir.y * arrow_size + dir.x * arrow_size * 0.4f);
247 draw_list->AddTriangleFilled(p2, arrow1, arrow2,
248 IM_COL32(150, 150, 150, 200));
249 }
250 }
251
252 // Draw nodes
253 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
254
255 for (const auto& node : graph->nodes()) {
256 if (hide_non_matching_ && !IsNodeVisible(node.id)) {
257 continue;
258 }
259
260 float nx = cx + node.pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f;
261 float ny = cy + node.pos_y * zoom_ - kNodeHeight * zoom_ * 0.5f;
262 float nw = kNodeWidth * zoom_;
263 float nh = kNodeHeight * zoom_;
264
265 ImVec2 node_min(nx, ny);
266 ImVec2 node_max(nx + nw, ny + nh);
267
268 // Color by status
269 ImU32 fill_color = GetStatusColor(node.status);
270 const bool selected = (node.id == selected_node_);
271 const bool query_match =
272 (HasNonEmptyQuery() && IsNodeQueryMatch(node.id));
273 ImU32 border_color = selected
274 ? IM_COL32(255, 255, 100, 255)
275 : (query_match ? IM_COL32(220, 220, 220, 255)
276 : IM_COL32(60, 60, 60, 255));
277
278 draw_list->AddRectFilled(node_min, node_max, fill_color, 8.0f * zoom_);
279 draw_list->AddRect(node_min, node_max, border_color, 8.0f * zoom_, 0,
280 2.0f * zoom_);
281
282 // Node text
283 float font_size = 11.0f * zoom_;
284 if (font_size >= 6.0f) {
285 // ID label
286 draw_list->AddText(nullptr, font_size,
287 ImVec2(nx + 6 * zoom_, ny + 4 * zoom_),
288 IM_COL32(200, 200, 200, 255), node.id.c_str());
289 // Name (truncated)
290 std::string display_name = node.name;
291 if (display_name.length() > 25) {
292 display_name = display_name.substr(0, 22) + "...";
293 }
294 draw_list->AddText(nullptr, font_size,
295 ImVec2(nx + 6 * zoom_, ny + 18 * zoom_),
296 IM_COL32(255, 255, 255, 255), display_name.c_str());
297 }
298
299 // Click detection
300 if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
301 if (mouse_pos.x >= node_min.x && mouse_pos.x <= node_max.x &&
302 mouse_pos.y >= node_min.y && mouse_pos.y <= node_max.y) {
303 selected_node_ = (selected_node_ == node.id) ? "" : node.id;
304 }
305 }
306 }
307
308 draw_list->PopClipRect();
309
310 // Detail sidebar
311 if (!selected_node_.empty() && sidebar_width > 0) {
312 ImGui::SameLine();
313 ImGui::BeginGroup();
314 DrawNodeDetail(*graph);
315 ImGui::EndGroup();
316 }
317 }
318
319 private:
320 static constexpr float kNodeWidth = 160.0f;
321 static constexpr float kNodeHeight = 40.0f;
322
324 switch (status) {
326 return IM_COL32(40, 120, 40, 220);
328 return IM_COL32(180, 160, 40, 220);
330 return IM_COL32(160, 40, 40, 220);
332 default:
333 return IM_COL32(60, 60, 60, 220);
334 }
335 }
336
338 const auto* node = graph.GetNode(selected_node_);
339 if (!node)
340 return;
341
342 ImGui::BeginChild("node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
343
344 ImGui::TextWrapped("%s", node->name.c_str());
345 ImGui::TextDisabled("%s", node->id.c_str());
346 ImGui::Separator();
347
348 if (!node->flags.empty()) {
349 ImGui::Text("Flags:");
350 for (const auto& flag : node->flags) {
351 if (!flag.value.empty()) {
352 ImGui::BulletText("%s = %s", flag.name.c_str(), flag.value.c_str());
353 } else {
354 ImGui::BulletText("%s", flag.name.c_str());
355 }
356 }
357 }
358
359 if (!node->locations.empty()) {
360 ImGui::Spacing();
361 ImGui::Text("Locations:");
362 for (size_t i = 0; i < node->locations.size(); ++i) {
363 const auto& loc = node->locations[i];
364 ImGui::PushID(static_cast<int>(i));
365
366 ImGui::BulletText("%s", loc.name.c_str());
367
368 if (auto room_id = ParseIntLoose(loc.room_id)) {
369 ImGui::SameLine();
370 if (ImGui::SmallButton("Room")) {
371 PublishJumpToRoom(*room_id);
372 }
373 }
374 if (auto map_id = ParseIntLoose(loc.overworld_id)) {
375 ImGui::SameLine();
376 if (ImGui::SmallButton("Map")) {
377 PublishJumpToMap(*map_id);
378 }
379 }
380
381 if (!loc.room_id.empty() || !loc.overworld_id.empty() ||
382 !loc.entrance_id.empty()) {
383 ImGui::TextDisabled(
384 "room=%s map=%s entrance=%s",
385 loc.room_id.empty() ? "-" : loc.room_id.c_str(),
386 loc.overworld_id.empty() ? "-" : loc.overworld_id.c_str(),
387 loc.entrance_id.empty() ? "-" : loc.entrance_id.c_str());
388 }
389
390 ImGui::PopID();
391 }
392 }
393
394 if (!node->text_ids.empty()) {
395 ImGui::Spacing();
396 ImGui::Text("Text IDs:");
397 for (size_t i = 0; i < node->text_ids.size(); ++i) {
398 const auto& tid = node->text_ids[i];
399 ImGui::PushID(static_cast<int>(i));
400
401 ImGui::BulletText("%s", tid.c_str());
402 ImGui::SameLine();
403 if (ImGui::SmallButton("Open")) {
404 if (auto msg_id = ParseIntLoose(tid)) {
405 PublishJumpToMessage(*msg_id);
406 }
407 }
408 ImGui::SameLine();
409 if (ImGui::SmallButton("Copy")) {
410 ImGui::SetClipboardText(tid.c_str());
411 }
412
413 ImGui::PopID();
414 }
415 }
416
417 if (!node->scripts.empty()) {
418 ImGui::Spacing();
419 ImGui::Text("Scripts:");
420 for (size_t i = 0; i < node->scripts.size(); ++i) {
421 const auto& script = node->scripts[i];
422 ImGui::PushID(static_cast<int>(i));
423 ImGui::BulletText("%s", script.c_str());
424 ImGui::SameLine();
425 if (ImGui::SmallButton("Open")) {
427 }
428 ImGui::SameLine();
429 if (ImGui::SmallButton("Copy")) {
430 ImGui::SetClipboardText(script.c_str());
431 }
432 ImGui::PopID();
433 }
434 }
435
436 if (!node->notes.empty()) {
437 ImGui::Spacing();
438 ImGui::TextWrapped("Notes: %s", node->notes.c_str());
439 }
440
441 ImGui::EndChild();
442 }
443
444 static std::optional<int> ParseIntLoose(const std::string& input) {
445 // Trim whitespace.
446 size_t start = input.find_first_not_of(" \t\r\n");
447 if (start == std::string::npos)
448 return std::nullopt;
449 size_t end = input.find_last_not_of(" \t\r\n");
450 std::string trimmed = input.substr(start, end - start + 1);
451
452 try {
453 size_t idx = 0;
454 int value = std::stoi(trimmed, &idx, /*base=*/0);
455 if (idx != trimmed.size())
456 return std::nullopt;
457 return value;
458 } catch (...) {
459 return std::nullopt;
460 }
461 }
462
463 void PublishJumpToRoom(int room_id) const {
464 if (auto* bus = ContentRegistry::Context::event_bus()) {
465 bus->Publish(JumpToRoomRequestEvent::Create(room_id));
466 }
467 }
468
469 void PublishJumpToMap(int map_id) const {
470 if (auto* bus = ContentRegistry::Context::event_bus()) {
471 bus->Publish(JumpToMapRequestEvent::Create(map_id));
472 }
473 }
474
475 void PublishJumpToMessage(int message_id) const {
476 if (auto* bus = ContentRegistry::Context::event_bus()) {
477 bus->Publish(JumpToMessageRequestEvent::Create(message_id));
478 }
479 }
480
481 void PublishJumpToAssemblySymbol(const std::string& symbol) const {
482 if (auto* bus = ContentRegistry::Context::event_bus()) {
483 bus->Publish(JumpToAssemblySymbolRequestEvent::Create(symbol));
484 }
485 }
486
488 if (!manifest_)
489 return 0;
490 const auto prog_opt =
494 if (!prog_opt.has_value())
495 return 0;
496
497 const auto& s = *prog_opt;
498 return static_cast<uint64_t>(s.crystal_bitfield) |
499 (static_cast<uint64_t>(s.game_state) << 8) |
500 (static_cast<uint64_t>(s.oosprog) << 16) |
501 (static_cast<uint64_t>(s.oosprog2) << 24) |
502 (static_cast<uint64_t>(s.side_quest) << 32) |
503 (static_cast<uint64_t>(s.pendants) << 40);
504 }
505
507 (void)graph;
508
509 ImGui::Text("Filter");
510 ImGui::SameLine();
511 ImGui::SetNextItemWidth(260.0f);
512 if (ImGui::InputTextWithHint("##story_graph_filter",
513 "Search id/name/text/script/flag/room...",
514 &filter_query_)) {
515 filter_dirty_ = true;
516 }
517 ImGui::SameLine();
518 if (ImGui::SmallButton("Clear")) {
519 if (!filter_query_.empty()) {
520 filter_query_.clear();
521 filter_dirty_ = true;
522 }
523 }
524
525 ImGui::SameLine();
526 if (ImGui::Checkbox("Hide non-matching", &hide_non_matching_)) {
527 // Hiding doesn't change matches, but it can invalidate selection.
528 filter_dirty_ = true;
529 }
530
531 ImGui::SameLine();
532 bool toggles_changed = false;
533 toggles_changed |= ImGui::Checkbox("Completed", &show_completed_);
534 ImGui::SameLine();
535 toggles_changed |= ImGui::Checkbox("Available", &show_available_);
536 ImGui::SameLine();
537 toggles_changed |= ImGui::Checkbox("Locked", &show_locked_);
538 ImGui::SameLine();
539 toggles_changed |= ImGui::Checkbox("Blocked", &show_blocked_);
540 if (toggles_changed) {
541 filter_dirty_ = true;
542 }
543 }
544
545 static uint8_t StatusMask(bool completed, bool available, bool locked,
546 bool blocked) {
547 uint8_t mask = 0;
548 if (completed)
549 mask |= 1u << 0;
550 if (available)
551 mask |= 1u << 1;
552 if (locked)
553 mask |= 1u << 2;
554 if (blocked)
555 mask |= 1u << 3;
556 return mask;
557 }
558
559 bool HasNonEmptyQuery() const { return !filter_query_.empty(); }
560
561 bool IsNodeVisible(const std::string& id) const {
562 auto it = node_visible_by_id_.find(id);
563 return it != node_visible_by_id_.end() ? it->second : true;
564 }
565
566 bool IsNodeQueryMatch(const std::string& id) const {
567 auto it = node_query_match_by_id_.find(id);
568 return it != node_query_match_by_id_.end() ? it->second : false;
569 }
570
572 const uint8_t status_mask = StatusMask(show_completed_, show_available_,
574 const uint64_t progress_fp = ComputeProgressionFingerprint();
575
576 const size_t node_count = graph.nodes().size();
577 if (!filter_dirty_ && node_count == last_node_count_ &&
579 status_mask == last_status_mask_ && progress_fp == last_progress_fp_) {
580 return;
581 }
582
583 last_node_count_ = node_count;
585 last_status_mask_ = status_mask;
586 last_progress_fp_ = progress_fp;
587 filter_dirty_ = false;
588
590 filter.query = filter_query_;
595
597 node_visible_by_id_.clear();
598 node_query_match_by_id_.reserve(node_count);
599 node_visible_by_id_.reserve(node_count);
600
601 for (const auto& node : graph.nodes()) {
602 const bool query_match =
604 const bool visible =
605 query_match && core::StoryNodeStatusAllowed(node.status, filter);
606 node_query_match_by_id_[node.id] = query_match;
607 node_visible_by_id_[node.id] = visible;
608 }
609
610 // If we're hiding nodes and the selection becomes invisible, clear it to
611 // avoid a "ghost sidebar" pointing at a filtered-out node.
612 if (hide_non_matching_ && !selected_node_.empty() &&
614 selected_node_.clear();
615 }
616 }
617
619 if (!manifest_)
620 return;
621
623 options.filters = {
624 {"SRAM (.srm)", "srm"},
625 {"All Files", "*"},
626 };
627
628 std::string file_path =
630 if (file_path.empty()) {
631 return;
632 }
633
634 auto state_or =
638 if (!state_or.ok()) {
639 last_srm_error_ = std::string(state_or.status().message());
640 return;
641 }
642
643 if (auto* backend = GetProgressionBackend()) {
644 backend->SetProgressionState(*manifest_, *state_or);
645 } else {
647 }
648 loaded_srm_path_ = file_path;
649 last_srm_error_.clear();
650
651 // Status coloring changed; refresh filter cache visibility.
652 filter_dirty_ = true;
653 }
654
656 if (!manifest_)
657 return;
658 if (auto* backend = GetProgressionBackend()) {
659 backend->ClearProgressionState(*manifest_);
660 } else {
662 }
663 loaded_srm_path_.clear();
664 last_srm_error_.clear();
665 filter_dirty_ = true;
666 }
667
669 const bool connected = live_client_ && live_client_->IsConnected();
670 if (ImGui::SmallButton("Sync Mesen")) {
671 live_refresh_pending_.store(false);
673 }
674 if (ImGui::IsItemHovered()) {
675 ImGui::SetTooltip("Read Oracle SRAM directly from connected Mesen2");
676 }
677
678 ImGui::SameLine();
679 ImGui::Checkbox("Live", &live_sync_enabled_);
680 if (live_sync_enabled_) {
681 ImGui::SameLine();
682 ImGui::SetNextItemWidth(70.0f);
683 ImGui::SliderFloat("##StoryGraphLiveInterval",
684 &live_refresh_interval_seconds_, 0.05f, 0.5f, "%.2fs");
685 }
686
687 ImGui::SameLine();
688 if (connected) {
689 ImGui::TextDisabled("Mesen: connected");
690 } else {
691 ImGui::TextDisabled("Mesen: disconnected");
692 }
693
694 if (!live_sync_error_.empty()) {
695 ImGui::SameLine();
696 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), "Live sync error");
697 if (ImGui::IsItemHovered()) {
698 ImGui::SetTooltip("%s", live_sync_error_.c_str());
699 }
700 }
701 }
702
705 if (client == live_client_) {
706 return;
707 }
708
710 live_client_ = std::move(client);
712 live_sync_error_.clear();
713
714 if (live_client_ && live_client_->IsConnected()) {
715 live_refresh_pending_.store(true);
716 }
717 }
718
720 if (!live_sync_enabled_) {
721 return;
722 }
723 if (!live_client_ || !live_client_->IsConnected()) {
725 return;
726 }
727
728 if (live_listener_id_ == 0) {
729 live_listener_id_ = live_client_->AddEventListener(
730 [this](const emu::mesen::MesenEvent& event) {
731 if (event.type == "frame_complete" ||
732 event.type == "breakpoint_hit" || event.type == "all") {
733 live_refresh_pending_.store(true);
734 }
735 });
736 }
737
739 return;
740 }
741
742 const double now = ImGui::GetTime();
743 if ((now - last_subscribe_attempt_time_) < 1.0) {
744 return;
745 }
747
748 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
749 if (!status.ok()) {
750 live_sync_error_ = std::string(status.message());
751 return;
752 }
753
755 live_sync_error_.clear();
756 live_refresh_pending_.store(true);
757 }
758
760 if (!live_sync_enabled_) {
761 return;
762 }
763 if (!live_refresh_pending_.load()) {
764 return;
765 }
766 const double now = ImGui::GetTime();
768 return;
769 }
772 }
773 live_refresh_pending_.store(false);
774 }
775
777 if (!manifest_) {
778 return false;
779 }
780 if (!live_client_ || !live_client_->IsConnected()) {
781 live_sync_error_ = "Mesen client is not connected";
782 return false;
783 }
784
785 auto* backend = GetProgressionBackend();
786 if (backend == nullptr) {
787 live_sync_error_ = "No hack workflow backend is available";
788 return false;
789 }
790
791 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
792 if (!state_or.ok()) {
793 live_sync_error_ = std::string(state_or.status().message());
794 return false;
795 }
796
797 backend->SetProgressionState(*manifest_, *state_or);
798 loaded_srm_path_ = "Mesen2 Live";
799 last_srm_error_.clear();
800 live_sync_error_.clear();
801 filter_dirty_ = true;
802 return true;
803 }
804
806 if (live_client_ && live_listener_id_ != 0) {
807 live_client_->RemoveEventListener(live_listener_id_);
808 }
811 }
812
816
820
824
826 if (!manifest_) {
827 return nullptr;
828 }
829 if (auto* backend = GetPlanningBackend()) {
830 return backend->GetStoryGraph(*manifest_);
831 }
833 }
834
836 std::string selected_node_;
837 float scroll_x_ = 0;
838 float scroll_y_ = 0;
839 float zoom_ = 1.0f;
840
841 // Filter state
842 std::string filter_query_;
843 bool hide_non_matching_ = false;
844 bool show_completed_ = true;
845 bool show_available_ = true;
846 bool show_locked_ = true;
847 bool show_blocked_ = true;
848
849 // Filter cache (recomputed only when query/toggles change)
850 bool filter_dirty_ = true;
853 uint8_t last_status_mask_ = 0;
854 std::unordered_map<std::string, bool> node_query_match_by_id_;
855 std::unordered_map<std::string, bool> node_visible_by_id_;
856
857 // SRAM import state (purely UI; the actual progression state lives in HackManifest).
858 std::string loaded_srm_path_;
859 std::string last_srm_error_;
860
861 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
865 std::atomic<bool> live_refresh_pending_{false};
869 std::string live_sync_error_;
870
871 uint64_t last_progress_fp_ = 0;
872};
873
874} // namespace yaze::editor
875
876#endif // YAZE_APP_EDITOR_HACK_ORACLE_UI_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
Interactive node graph of Oracle narrative progression.
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.
Base interface for all logical window content components.
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