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
18#include "app/gui/core/icons.h"
19#include "core/hack_manifest.h"
21#include "core/project.h"
24#include "imgui/imgui.h"
25#include "imgui/misc/cpp/imgui_stdlib.h"
26#include "util/file_util.h"
27
28namespace yaze::editor {
29
42 public:
45
49 void SetManifest(core::HackManifest* manifest) { manifest_ = manifest; }
50
51 std::string GetId() const override { return "oracle.story_event_graph"; }
52 std::string GetDisplayName() const override { return "Story Event Graph"; }
53 std::string GetIcon() const override { return ICON_MD_ACCOUNT_TREE; }
54 std::string GetEditorCategory() const override { return "Oracle"; }
57 }
58 float GetPreferredWidth() const override { return 600.0f; }
59
60 void Draw(bool* /*p_open*/) override {
61 // Lazily resolve the manifest from the project context
62 if (!manifest_) {
64 if (project && project->hack_manifest.loaded()) {
65 manifest_ = &project->hack_manifest;
66 }
67 }
68
70 ImGui::TextDisabled("No Oracle project loaded");
71 ImGui::TextDisabled(
72 "Open a project with a hack manifest to view story events.");
73 return;
74 }
75
79
80 const auto& graph = manifest_->project_registry().story_events;
81 if (!graph.loaded()) {
82 ImGui::TextDisabled("No story events data available");
83 return;
84 }
85
86 // Controls row
87 if (ImGui::Button("Reset View")) {
88 scroll_x_ = 0;
89 scroll_y_ = 0;
90 zoom_ = 1.0f;
91 }
92 ImGui::SameLine();
93 ImGui::SliderFloat("Zoom", &zoom_, 0.3f, 2.0f, "%.1f");
94 ImGui::SameLine();
95 ImGui::Text("Nodes: %zu Edges: %zu", graph.nodes().size(),
96 graph.edges().size());
97 ImGui::SameLine();
98 const auto prog_opt = manifest_->oracle_progression_state();
99 if (prog_opt.has_value()) {
100 ImGui::TextDisabled("Crystals: %d State: %s",
101 prog_opt->GetCrystalCount(),
102 prog_opt->GetGameStateName().c_str());
103 } else {
104 ImGui::TextDisabled("No SRAM loaded");
105 }
106
107 ImGui::SameLine();
108 if (ImGui::SmallButton("Import .srm...")) {
110 }
111 ImGui::SameLine();
112 if (ImGui::SmallButton("Clear SRAM")) {
114 }
115 ImGui::SameLine();
117 if (!loaded_srm_path_.empty()) {
118 const std::filesystem::path p(loaded_srm_path_);
119 ImGui::SameLine();
120 ImGui::TextDisabled("SRM: %s", p.filename().string().c_str());
121 if (ImGui::IsItemHovered()) {
122 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
123 }
124 }
125
126 if (!last_srm_error_.empty()) {
127 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "SRM error: %s",
128 last_srm_error_.c_str());
129 }
130
131 ImGui::Separator();
132
133 DrawFilterControls(graph);
134 UpdateFilterCache(graph);
135
136 ImGui::Separator();
137
138 // Main canvas area
139 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
140 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
141
142 // Reserve space for detail sidebar if a node is selected
143 float sidebar_width = selected_node_.empty() ? 0.0f : 250.0f;
144 canvas_size.x -= sidebar_width;
145
146 if (canvas_size.x < 100 || canvas_size.y < 100) return;
147
148 ImGui::InvisibleButton("story_canvas", canvas_size,
149 ImGuiButtonFlags_MouseButtonLeft |
150 ImGuiButtonFlags_MouseButtonRight);
151
152 bool is_hovered = ImGui::IsItemHovered();
153 bool is_active = ImGui::IsItemActive();
154
155 // Pan with right mouse button
156 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
157 ImVec2 delta = ImGui::GetIO().MouseDelta;
158 scroll_x_ += delta.x;
159 scroll_y_ += delta.y;
160 }
161
162 // Zoom with scroll wheel
163 if (is_hovered) {
164 float wheel = ImGui::GetIO().MouseWheel;
165 if (wheel != 0.0f) {
166 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
167 if (zoom_ < 0.3f) zoom_ = 0.3f;
168 if (zoom_ > 2.0f) zoom_ = 2.0f;
169 }
170 }
171
172 ImDrawList* draw_list = ImGui::GetWindowDrawList();
173
174 // Clip to canvas
175 draw_list->PushClipRect(canvas_pos,
176 ImVec2(canvas_pos.x + canvas_size.x,
177 canvas_pos.y + canvas_size.y),
178 true);
179
180 // Center offset
181 float cx = canvas_pos.x + canvas_size.x * 0.5f + scroll_x_;
182 float cy = canvas_pos.y + canvas_size.y * 0.5f + scroll_y_;
183
184 // Draw edges first (behind nodes)
185 for (const auto& edge : graph.edges()) {
186 const auto* from_node = graph.GetNode(edge.from);
187 const auto* to_node = graph.GetNode(edge.to);
188 if (!from_node || !to_node) continue;
189 if (hide_non_matching_) {
190 if (!IsNodeVisible(edge.from) || !IsNodeVisible(edge.to)) continue;
191 }
192
193 ImVec2 p1(cx + from_node->pos_x * zoom_ + kNodeWidth * zoom_ * 0.5f,
194 cy + from_node->pos_y * zoom_);
195 ImVec2 p2(cx + to_node->pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f,
196 cy + to_node->pos_y * zoom_);
197
198 // Bezier control points
199 float ctrl_dx = (p2.x - p1.x) * 0.4f;
200 ImVec2 cp1(p1.x + ctrl_dx, p1.y);
201 ImVec2 cp2(p2.x - ctrl_dx, p2.y);
202
203 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
204 1.5f * zoom_);
205
206 // Arrow head
207 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
208 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
209 if (len > 0) {
210 dir.x /= len;
211 dir.y /= len;
212 float arrow_size = 8.0f * zoom_;
213 ImVec2 arrow1(p2.x - dir.x * arrow_size + dir.y * arrow_size * 0.4f,
214 p2.y - dir.y * arrow_size - dir.x * arrow_size * 0.4f);
215 ImVec2 arrow2(p2.x - dir.x * arrow_size - dir.y * arrow_size * 0.4f,
216 p2.y - dir.y * arrow_size + dir.x * arrow_size * 0.4f);
217 draw_list->AddTriangleFilled(p2, arrow1, arrow2,
218 IM_COL32(150, 150, 150, 200));
219 }
220 }
221
222 // Draw nodes
223 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
224
225 for (const auto& node : graph.nodes()) {
226 if (hide_non_matching_ && !IsNodeVisible(node.id)) {
227 continue;
228 }
229
230 float nx = cx + node.pos_x * zoom_ - kNodeWidth * zoom_ * 0.5f;
231 float ny = cy + node.pos_y * zoom_ - kNodeHeight * zoom_ * 0.5f;
232 float nw = kNodeWidth * zoom_;
233 float nh = kNodeHeight * zoom_;
234
235 ImVec2 node_min(nx, ny);
236 ImVec2 node_max(nx + nw, ny + nh);
237
238 // Color by status
239 ImU32 fill_color = GetStatusColor(node.status);
240 const bool selected = (node.id == selected_node_);
241 const bool query_match = (HasNonEmptyQuery() && IsNodeQueryMatch(node.id));
242 ImU32 border_color = selected ? IM_COL32(255, 255, 100, 255)
243 : (query_match ? IM_COL32(220, 220, 220, 255)
244 : IM_COL32(60, 60, 60, 255));
245
246 draw_list->AddRectFilled(node_min, node_max, fill_color, 8.0f * zoom_);
247 draw_list->AddRect(node_min, node_max, border_color, 8.0f * zoom_,
248 0, 2.0f * zoom_);
249
250 // Node text
251 float font_size = 11.0f * zoom_;
252 if (font_size >= 6.0f) {
253 // ID label
254 draw_list->AddText(nullptr, font_size,
255 ImVec2(nx + 6 * zoom_, ny + 4 * zoom_),
256 IM_COL32(200, 200, 200, 255),
257 node.id.c_str());
258 // Name (truncated)
259 std::string display_name = node.name;
260 if (display_name.length() > 25) {
261 display_name = display_name.substr(0, 22) + "...";
262 }
263 draw_list->AddText(nullptr, font_size,
264 ImVec2(nx + 6 * zoom_, ny + 18 * zoom_),
265 IM_COL32(255, 255, 255, 255),
266 display_name.c_str());
267 }
268
269 // Click detection
270 if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
271 if (mouse_pos.x >= node_min.x && mouse_pos.x <= node_max.x &&
272 mouse_pos.y >= node_min.y && mouse_pos.y <= node_max.y) {
273 selected_node_ = (selected_node_ == node.id) ? "" : node.id;
274 }
275 }
276 }
277
278 draw_list->PopClipRect();
279
280 // Detail sidebar
281 if (!selected_node_.empty() && sidebar_width > 0) {
282 ImGui::SameLine();
283 ImGui::BeginGroup();
284 DrawNodeDetail(graph);
285 ImGui::EndGroup();
286 }
287 }
288
289 private:
290 static constexpr float kNodeWidth = 160.0f;
291 static constexpr float kNodeHeight = 40.0f;
292
294 switch (status) {
296 return IM_COL32(40, 120, 40, 220);
298 return IM_COL32(180, 160, 40, 220);
300 return IM_COL32(160, 40, 40, 220);
302 default:
303 return IM_COL32(60, 60, 60, 220);
304 }
305 }
306
308 const auto* node = graph.GetNode(selected_node_);
309 if (!node) return;
310
311 ImGui::BeginChild("node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
312
313 ImGui::TextWrapped("%s", node->name.c_str());
314 ImGui::TextDisabled("%s", node->id.c_str());
315 ImGui::Separator();
316
317 if (!node->flags.empty()) {
318 ImGui::Text("Flags:");
319 for (const auto& flag : node->flags) {
320 if (!flag.value.empty()) {
321 ImGui::BulletText("%s = %s", flag.name.c_str(), flag.value.c_str());
322 } else {
323 ImGui::BulletText("%s", flag.name.c_str());
324 }
325 }
326 }
327
328 if (!node->locations.empty()) {
329 ImGui::Spacing();
330 ImGui::Text("Locations:");
331 for (size_t i = 0; i < node->locations.size(); ++i) {
332 const auto& loc = node->locations[i];
333 ImGui::PushID(static_cast<int>(i));
334
335 ImGui::BulletText("%s", loc.name.c_str());
336
337 if (auto room_id = ParseIntLoose(loc.room_id)) {
338 ImGui::SameLine();
339 if (ImGui::SmallButton("Room")) {
340 PublishJumpToRoom(*room_id);
341 }
342 }
343 if (auto map_id = ParseIntLoose(loc.overworld_id)) {
344 ImGui::SameLine();
345 if (ImGui::SmallButton("Map")) {
346 PublishJumpToMap(*map_id);
347 }
348 }
349
350 if (!loc.room_id.empty() || !loc.overworld_id.empty() ||
351 !loc.entrance_id.empty()) {
352 ImGui::TextDisabled("room=%s map=%s entrance=%s",
353 loc.room_id.empty() ? "-" : loc.room_id.c_str(),
354 loc.overworld_id.empty() ? "-" : loc.overworld_id.c_str(),
355 loc.entrance_id.empty() ? "-" : loc.entrance_id.c_str());
356 }
357
358 ImGui::PopID();
359 }
360 }
361
362 if (!node->text_ids.empty()) {
363 ImGui::Spacing();
364 ImGui::Text("Text IDs:");
365 for (size_t i = 0; i < node->text_ids.size(); ++i) {
366 const auto& tid = node->text_ids[i];
367 ImGui::PushID(static_cast<int>(i));
368
369 ImGui::BulletText("%s", tid.c_str());
370 ImGui::SameLine();
371 if (ImGui::SmallButton("Open")) {
372 if (auto msg_id = ParseIntLoose(tid)) {
373 PublishJumpToMessage(*msg_id);
374 }
375 }
376 ImGui::SameLine();
377 if (ImGui::SmallButton("Copy")) {
378 ImGui::SetClipboardText(tid.c_str());
379 }
380
381 ImGui::PopID();
382 }
383 }
384
385 if (!node->scripts.empty()) {
386 ImGui::Spacing();
387 ImGui::Text("Scripts:");
388 for (size_t i = 0; i < node->scripts.size(); ++i) {
389 const auto& script = node->scripts[i];
390 ImGui::PushID(static_cast<int>(i));
391 ImGui::BulletText("%s", script.c_str());
392 ImGui::SameLine();
393 if (ImGui::SmallButton("Open")) {
395 }
396 ImGui::SameLine();
397 if (ImGui::SmallButton("Copy")) {
398 ImGui::SetClipboardText(script.c_str());
399 }
400 ImGui::PopID();
401 }
402 }
403
404 if (!node->notes.empty()) {
405 ImGui::Spacing();
406 ImGui::TextWrapped("Notes: %s", node->notes.c_str());
407 }
408
409 ImGui::EndChild();
410 }
411
412 static std::optional<int> ParseIntLoose(const std::string& input) {
413 // Trim whitespace.
414 size_t start = input.find_first_not_of(" \t\r\n");
415 if (start == std::string::npos) return std::nullopt;
416 size_t end = input.find_last_not_of(" \t\r\n");
417 std::string trimmed = input.substr(start, end - start + 1);
418
419 try {
420 size_t idx = 0;
421 int value = std::stoi(trimmed, &idx, /*base=*/0);
422 if (idx != trimmed.size()) return std::nullopt;
423 return value;
424 } catch (...) {
425 return std::nullopt;
426 }
427 }
428
429 void PublishJumpToRoom(int room_id) const {
430 if (auto* bus = ContentRegistry::Context::event_bus()) {
431 bus->Publish(JumpToRoomRequestEvent::Create(room_id));
432 }
433 }
434
435 void PublishJumpToMap(int map_id) const {
436 if (auto* bus = ContentRegistry::Context::event_bus()) {
437 bus->Publish(JumpToMapRequestEvent::Create(map_id));
438 }
439 }
440
441 void PublishJumpToMessage(int message_id) const {
442 if (auto* bus = ContentRegistry::Context::event_bus()) {
443 bus->Publish(JumpToMessageRequestEvent::Create(message_id));
444 }
445 }
446
447 void PublishJumpToAssemblySymbol(const std::string& symbol) const {
448 if (auto* bus = ContentRegistry::Context::event_bus()) {
449 bus->Publish(JumpToAssemblySymbolRequestEvent::Create(symbol));
450 }
451 }
452
454 if (!manifest_) return 0;
455 const auto prog_opt = manifest_->oracle_progression_state();
456 if (!prog_opt.has_value()) return 0;
457
458 const auto& s = *prog_opt;
459 return static_cast<uint64_t>(s.crystal_bitfield) |
460 (static_cast<uint64_t>(s.game_state) << 8) |
461 (static_cast<uint64_t>(s.oosprog) << 16) |
462 (static_cast<uint64_t>(s.oosprog2) << 24) |
463 (static_cast<uint64_t>(s.side_quest) << 32) |
464 (static_cast<uint64_t>(s.pendants) << 40);
465 }
466
468 (void)graph;
469
470 ImGui::Text("Filter");
471 ImGui::SameLine();
472 ImGui::SetNextItemWidth(260.0f);
473 if (ImGui::InputTextWithHint("##story_graph_filter",
474 "Search id/name/text/script/flag/room...",
475 &filter_query_)) {
476 filter_dirty_ = true;
477 }
478 ImGui::SameLine();
479 if (ImGui::SmallButton("Clear")) {
480 if (!filter_query_.empty()) {
481 filter_query_.clear();
482 filter_dirty_ = true;
483 }
484 }
485
486 ImGui::SameLine();
487 if (ImGui::Checkbox("Hide non-matching", &hide_non_matching_)) {
488 // Hiding doesn't change matches, but it can invalidate selection.
489 filter_dirty_ = true;
490 }
491
492 ImGui::SameLine();
493 bool toggles_changed = false;
494 toggles_changed |= ImGui::Checkbox("Completed", &show_completed_);
495 ImGui::SameLine();
496 toggles_changed |= ImGui::Checkbox("Available", &show_available_);
497 ImGui::SameLine();
498 toggles_changed |= ImGui::Checkbox("Locked", &show_locked_);
499 ImGui::SameLine();
500 toggles_changed |= ImGui::Checkbox("Blocked", &show_blocked_);
501 if (toggles_changed) {
502 filter_dirty_ = true;
503 }
504 }
505
506 static uint8_t StatusMask(bool completed, bool available, bool locked,
507 bool blocked) {
508 uint8_t mask = 0;
509 if (completed) mask |= 1u << 0;
510 if (available) mask |= 1u << 1;
511 if (locked) mask |= 1u << 2;
512 if (blocked) mask |= 1u << 3;
513 return mask;
514 }
515
516 bool HasNonEmptyQuery() const { return !filter_query_.empty(); }
517
518 bool IsNodeVisible(const std::string& id) const {
519 auto it = node_visible_by_id_.find(id);
520 return it != node_visible_by_id_.end() ? it->second : true;
521 }
522
523 bool IsNodeQueryMatch(const std::string& id) const {
524 auto it = node_query_match_by_id_.find(id);
525 return it != node_query_match_by_id_.end() ? it->second : false;
526 }
527
529 const uint8_t status_mask =
531 const uint64_t progress_fp = ComputeProgressionFingerprint();
532
533 const size_t node_count = graph.nodes().size();
534 if (!filter_dirty_ && node_count == last_node_count_ &&
536 progress_fp == last_progress_fp_) {
537 return;
538 }
539
540 last_node_count_ = node_count;
542 last_status_mask_ = status_mask;
543 last_progress_fp_ = progress_fp;
544 filter_dirty_ = false;
545
547 filter.query = filter_query_;
552
554 node_visible_by_id_.clear();
555 node_query_match_by_id_.reserve(node_count);
556 node_visible_by_id_.reserve(node_count);
557
558 for (const auto& node : graph.nodes()) {
559 const bool query_match = core::StoryEventNodeMatchesQuery(node, filter.query);
560 const bool visible =
561 query_match && core::StoryNodeStatusAllowed(node.status, filter);
562 node_query_match_by_id_[node.id] = query_match;
563 node_visible_by_id_[node.id] = visible;
564 }
565
566 // If we're hiding nodes and the selection becomes invisible, clear it to
567 // avoid a "ghost sidebar" pointing at a filtered-out node.
568 if (hide_non_matching_ && !selected_node_.empty() &&
570 selected_node_.clear();
571 }
572 }
573
575 if (!manifest_) return;
576
578 options.filters = {
579 {"SRAM (.srm)", "srm"},
580 {"All Files", "*"},
581 };
582
583 std::string file_path =
585 if (file_path.empty()) {
586 return;
587 }
588
589 auto state_or = core::LoadOracleProgressionFromSrmFile(file_path);
590 if (!state_or.ok()) {
591 last_srm_error_ = std::string(state_or.status().message());
592 return;
593 }
594
596 loaded_srm_path_ = file_path;
597 last_srm_error_.clear();
598
599 // Status coloring changed; refresh filter cache visibility.
600 filter_dirty_ = true;
601 }
602
604 if (!manifest_) return;
606 loaded_srm_path_.clear();
607 last_srm_error_.clear();
608 filter_dirty_ = true;
609 }
610
612 const bool connected = live_client_ && live_client_->IsConnected();
613 if (ImGui::SmallButton("Sync Mesen")) {
614 live_refresh_pending_.store(false);
616 }
617 if (ImGui::IsItemHovered()) {
618 ImGui::SetTooltip("Read Oracle SRAM directly from connected Mesen2");
619 }
620
621 ImGui::SameLine();
622 ImGui::Checkbox("Live", &live_sync_enabled_);
623 if (live_sync_enabled_) {
624 ImGui::SameLine();
625 ImGui::SetNextItemWidth(70.0f);
626 ImGui::SliderFloat("##StoryGraphLiveInterval", &live_refresh_interval_seconds_,
627 0.05f, 0.5f, "%.2fs");
628 }
629
630 ImGui::SameLine();
631 if (connected) {
632 ImGui::TextDisabled("Mesen: connected");
633 } else {
634 ImGui::TextDisabled("Mesen: disconnected");
635 }
636
637 if (!live_sync_error_.empty()) {
638 ImGui::SameLine();
639 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), "Live sync error");
640 if (ImGui::IsItemHovered()) {
641 ImGui::SetTooltip("%s", live_sync_error_.c_str());
642 }
643 }
644 }
645
648 if (client == live_client_) {
649 return;
650 }
651
653 live_client_ = std::move(client);
655 live_sync_error_.clear();
656
657 if (live_client_ && live_client_->IsConnected()) {
658 live_refresh_pending_.store(true);
659 }
660 }
661
663 if (!live_sync_enabled_) {
664 return;
665 }
666 if (!live_client_ || !live_client_->IsConnected()) {
668 return;
669 }
670
671 if (live_listener_id_ == 0) {
673 live_client_->AddEventListener([this](const emu::mesen::MesenEvent& event) {
674 if (event.type == "frame_complete" ||
675 event.type == "breakpoint_hit" || event.type == "all") {
676 live_refresh_pending_.store(true);
677 }
678 });
679 }
680
682 return;
683 }
684
685 const double now = ImGui::GetTime();
686 if ((now - last_subscribe_attempt_time_) < 1.0) {
687 return;
688 }
690
691 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
692 if (!status.ok()) {
693 live_sync_error_ = std::string(status.message());
694 return;
695 }
696
698 live_sync_error_.clear();
699 live_refresh_pending_.store(true);
700 }
701
703 if (!live_sync_enabled_) {
704 return;
705 }
706 if (!live_refresh_pending_.load()) {
707 return;
708 }
709 const double now = ImGui::GetTime();
711 return;
712 }
715 }
716 live_refresh_pending_.store(false);
717 }
718
720 if (!manifest_) {
721 return false;
722 }
723 if (!live_client_ || !live_client_->IsConnected()) {
724 live_sync_error_ = "Mesen client is not connected";
725 return false;
726 }
727
728 constexpr uint32_t kBaseAddress = 0x7EF000;
729 constexpr uint16_t kStartOffset = core::OracleProgressionState::kPendantOffset;
730 constexpr uint16_t kEndOffset = core::OracleProgressionState::kSideQuestOffset;
731 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
732 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
733
734 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
735 if (!bytes_or.ok()) {
736 live_sync_error_ = std::string(bytes_or.status().message());
737 return false;
738 }
739 if (bytes_or->size() < kReadLength) {
740 live_sync_error_ = "SRAM read returned truncated data";
741 return false;
742 }
743
745 const auto read_byte = [&](uint16_t offset) -> uint8_t {
746 return (*bytes_or)[offset - kStartOffset];
747 };
748
750 state.crystal_bitfield =
756
758 loaded_srm_path_ = "Mesen2 Live";
759 last_srm_error_.clear();
760 live_sync_error_.clear();
761 filter_dirty_ = true;
762 return true;
763 }
764
766 if (live_client_ && live_listener_id_ != 0) {
767 live_client_->RemoveEventListener(live_listener_id_);
768 }
771 }
772
774 std::string selected_node_;
775 float scroll_x_ = 0;
776 float scroll_y_ = 0;
777 float zoom_ = 1.0f;
778
779 // Filter state
780 std::string filter_query_;
781 bool hide_non_matching_ = false;
782 bool show_completed_ = true;
783 bool show_available_ = true;
784 bool show_locked_ = true;
785 bool show_blocked_ = true;
786
787 // Filter cache (recomputed only when query/toggles change)
788 bool filter_dirty_ = true;
791 uint8_t last_status_mask_ = 0;
792 std::unordered_map<std::string, bool> node_query_match_by_id_;
793 std::unordered_map<std::string, bool> node_visible_by_id_;
794
795 // SRAM import state (purely UI; the actual progression state lives in HackManifest).
796 std::string loaded_srm_path_;
797 std::string last_srm_error_;
798
799 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
803 std::atomic<bool> live_refresh_pending_{false};
807 std::string live_sync_error_;
808
809 uint64_t last_progress_fp_ = 0;
810};
811
812} // namespace yaze::editor
813
814#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
Base interface for all logical panel components.
Interactive node graph of Oracle narrative progression.
std::unordered_map< std::string, bool > node_query_match_by_id_
bool IsNodeQueryMatch(const std::string &id) const
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.
void PublishJumpToMessage(int message_id) const
PanelCategory GetPanelCategory() const override
Get the lifecycle category for this panel.
emu::mesen::EventListenerId live_listener_id_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
std::unordered_map< std::string, bool > node_visible_by_id_
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)
static std::optional< int > ParseIntLoose(const std::string &input)
void UpdateFilterCache(const core::StoryEventGraph &graph)
std::shared_ptr< emu::mesen::MesenSocketClient > live_client_
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.
::yaze::project::YazeProject * current_project()
Get the current project instance.
Editors are the view controllers for the application.
PanelCategory
Defines lifecycle behavior for editor panels.
@ CrossEditor
User can pin to persist across editors.
Oracle of Secrets game progression state parsed from SRAM.
static constexpr uint16_t kSideQuestOffset
static constexpr uint16_t kPendantOffset
static constexpr uint16_t kGameStateOffset
static constexpr uint16_t kOosProgOffset
static constexpr uint16_t kOosProg2Offset
static constexpr uint16_t kCrystalOffset
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