1#ifndef YAZE_APP_EDITOR_ORACLE_PANELS_STORY_EVENT_GRAPH_PANEL_H
2#define YAZE_APP_EDITOR_ORACLE_PANELS_STORY_EVENT_GRAPH_PANEL_H
11#include <unordered_map>
25#include "imgui/imgui.h"
26#include "imgui/misc/cpp/imgui_stdlib.h"
42class StoryEventGraphPanel :
public WindowContent {
52 std::string
GetId()
const override {
return "oracle.story_event_graph"; }
58 return "Inspect narrative and progression dependencies for the active project";
63 return project !=
nullptr && project->project_opened() && backend !=
nullptr;
66 return "Story graph data is not available for the active hack project";
73 void Draw(
bool* )
override {
83 manifest_ = backend->ResolveManifest(project);
84 }
else if (project && project->hack_manifest.loaded()) {
90 ImGui::TextDisabled(
"No hack project loaded");
92 "Open a project with a hack manifest to view story events.");
101 if (graph ==
nullptr || !graph->loaded()) {
102 ImGui::TextDisabled(
"No story events data available");
107 if (ImGui::Button(
"Reset View")) {
113 ImGui::SliderFloat(
"Zoom", &
zoom_, 0.3f, 2.0f,
"%.1f");
115 ImGui::Text(
"Nodes: %zu Edges: %zu", graph->nodes().size(),
116 graph->edges().size());
121 if (prog_opt.has_value()) {
122 ImGui::TextDisabled(
"Crystals: %d State: %s",
123 prog_opt->GetCrystalCount(),
124 prog_opt->GetGameStateName().c_str());
126 ImGui::TextDisabled(
"No SRAM loaded");
130 if (ImGui::SmallButton(
"Import .srm...")) {
134 if (ImGui::SmallButton(
"Clear SRAM")) {
142 ImGui::TextDisabled(
"SRM: %s", p.filename().string().c_str());
143 if (ImGui::IsItemHovered()) {
149 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f),
"SRM error: %s",
161 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
162 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
166 canvas_size.x -= sidebar_width;
168 if (canvas_size.x < 100 || canvas_size.y < 100)
return;
170 ImGui::InvisibleButton(
"story_canvas", canvas_size,
171 ImGuiButtonFlags_MouseButtonLeft |
172 ImGuiButtonFlags_MouseButtonRight);
174 bool is_hovered = ImGui::IsItemHovered();
175 bool is_active = ImGui::IsItemActive();
178 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
179 ImVec2 delta = ImGui::GetIO().MouseDelta;
186 float wheel = ImGui::GetIO().MouseWheel;
188 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
194 ImDrawList* draw_list = ImGui::GetWindowDrawList();
197 draw_list->PushClipRect(canvas_pos,
198 ImVec2(canvas_pos.x + canvas_size.x,
199 canvas_pos.y + canvas_size.y),
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_;
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;
216 cy + from_node->pos_y *
zoom_);
218 cy + to_node->pos_y *
zoom_);
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);
225 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
229 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
230 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
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));
245 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
247 for (
const auto& node : graph->nodes()) {
257 ImVec2 node_min(nx, ny);
258 ImVec2 node_max(nx + nw, ny + nh);
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));
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_,
273 float font_size = 11.0f *
zoom_;
274 if (font_size >= 6.0f) {
276 draw_list->AddText(
nullptr, font_size,
278 IM_COL32(200, 200, 200, 255),
281 std::string display_name = node.name;
282 if (display_name.length() > 25) {
283 display_name = display_name.substr(0, 22) +
"...";
285 draw_list->AddText(
nullptr, font_size,
287 IM_COL32(255, 255, 255, 255),
288 display_name.c_str());
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) {
300 draw_list->PopClipRect();
318 return IM_COL32(40, 120, 40, 220);
320 return IM_COL32(180, 160, 40, 220);
322 return IM_COL32(160, 40, 40, 220);
325 return IM_COL32(60, 60, 60, 220);
333 ImGui::BeginChild(
"node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
335 ImGui::TextWrapped(
"%s", node->name.c_str());
336 ImGui::TextDisabled(
"%s", node->id.c_str());
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());
345 ImGui::BulletText(
"%s", flag.name.c_str());
350 if (!node->locations.empty()) {
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));
357 ImGui::BulletText(
"%s", loc.name.c_str());
361 if (ImGui::SmallButton(
"Room")) {
367 if (ImGui::SmallButton(
"Map")) {
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());
384 if (!node->text_ids.empty()) {
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));
391 ImGui::BulletText(
"%s", tid.c_str());
393 if (ImGui::SmallButton(
"Open")) {
399 if (ImGui::SmallButton(
"Copy")) {
400 ImGui::SetClipboardText(tid.c_str());
407 if (!node->scripts.empty()) {
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());
415 if (ImGui::SmallButton(
"Open")) {
419 if (ImGui::SmallButton(
"Copy")) {
420 ImGui::SetClipboardText(script.c_str());
426 if (!node->notes.empty()) {
428 ImGui::TextWrapped(
"Notes: %s", node->notes.c_str());
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);
443 int value = std::stoi(trimmed, &idx, 0);
444 if (idx != trimmed.size())
return std::nullopt;
480 if (!prog_opt.has_value())
return 0;
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);
494 ImGui::Text(
"Filter");
496 ImGui::SetNextItemWidth(260.0f);
497 if (ImGui::InputTextWithHint(
"##story_graph_filter",
498 "Search id/name/text/script/flag/room...",
503 if (ImGui::SmallButton(
"Clear")) {
517 bool toggles_changed =
false;
522 toggles_changed |= ImGui::Checkbox(
"Locked", &
show_locked_);
524 toggles_changed |= ImGui::Checkbox(
"Blocked", &
show_blocked_);
525 if (toggles_changed) {
530 static uint8_t
StatusMask(
bool completed,
bool available,
bool locked,
533 if (completed) mask |= 1u << 0;
534 if (available) mask |= 1u << 1;
535 if (locked) mask |= 1u << 2;
536 if (blocked) mask |= 1u << 3;
553 const uint8_t status_mask =
557 const size_t node_count = graph.
nodes().size();
582 for (
const auto& node : graph.
nodes()) {
603 {
"SRAM (.srm)",
"srm"},
607 std::string file_path =
609 if (file_path.empty()) {
617 if (!state_or.ok()) {
623 backend->SetProgressionState(*
manifest_, *state_or);
637 backend->ClearProgressionState(*
manifest_);
648 if (ImGui::SmallButton(
"Sync Mesen")) {
652 if (ImGui::IsItemHovered()) {
653 ImGui::SetTooltip(
"Read Oracle SRAM directly from connected Mesen2");
660 ImGui::SetNextItemWidth(70.0f);
662 0.05f, 0.5f,
"%.2fs");
667 ImGui::TextDisabled(
"Mesen: connected");
669 ImGui::TextDisabled(
"Mesen: disconnected");
674 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f),
"Live sync error");
675 if (ImGui::IsItemHovered()) {
709 if (event.
type ==
"frame_complete" ||
710 event.
type ==
"breakpoint_hit" || event.
type ==
"all") {
711 live_refresh_pending_.store(true);
720 const double now = ImGui::GetTime();
726 auto status =
live_client_->Subscribe({
"frame_complete",
"breakpoint_hit"});
744 const double now = ImGui::GetTime();
764 if (backend ==
nullptr) {
769 auto state_or = backend->ReadProgressionStateFromLiveSram(*
live_client_);
770 if (!state_or.ok()) {
775 backend->SetProgressionState(*
manifest_, *state_or);
808 return backend->GetStoryGraph(*
manifest_);
839 std::shared_ptr<emu::mesen::MesenSocketClient>
live_client_;
Loads and queries the hack manifest JSON for yaze-ASM integration.
void ClearOracleProgressionState()
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
std::string selected_node_
workflow::HackWorkflowBackend * GetWorkflowBackend() const
float live_refresh_interval_seconds_
WindowLifecycle GetWindowLifecycle() const override
Get the lifecycle category for this window.
core::HackManifest * manifest_
static constexpr float kNodeHeight
bool IsNodeQueryMatch(const std::string &id) const
const core::StoryEventGraph * GetStoryGraph() const
void ImportOracleSramFromFileDialog()
std::unordered_map< std::string, bool > node_visible_by_id_
std::atomic< bool > live_refresh_pending_
void ProcessPendingLiveRefresh()
bool IsNodeVisible(const std::string &id) const
std::string GetId() const override
Unique identifier for this panel.
void DrawFilterControls(const core::StoryEventGraph &graph)
void PublishJumpToRoom(int room_id) const
void DetachLiveListener()
bool HasNonEmptyQuery() const
void ClearOracleSramState()
std::string loaded_srm_path_
std::string last_srm_error_
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
double last_live_refresh_time_
uint8_t last_status_mask_
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
uint64_t last_progress_fp_
bool RefreshStateFromLiveSram()
~StoryEventGraphPanel() override
std::string last_filter_query_
std::string GetIcon() const override
Material Design icon for this panel.
StoryEventGraphPanel()=default
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
void RefreshLiveClientBinding()
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
std::string filter_query_
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)
void EnsureLiveSubscription()
void DrawLiveSyncControls()
bool IsEnabled() const override
Check if this panel is currently enabled.
void PublishJumpToMap(int map_id) const
static std::optional< int > ParseIntLoose(const std::string &input)
static constexpr float kNodeWidth
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 live_sync_error_
bool live_subscription_active_
std::string GetDisabledTooltip() const override
Get tooltip text when panel is disabled.
double last_subscribe_attempt_time_
uint64_t ComputeProgressionFingerprint() const
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
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.
StoryEventGraph story_events
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