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
11#include <unordered_map>
25#include "imgui/imgui.h"
26#include "imgui/misc/cpp/imgui_stdlib.h"
52 std::string
GetId()
const override {
return "oracle.story_event_graph"; }
58 return "Inspect narrative and progression dependencies for the active "
64 return project !=
nullptr && project->project_opened() &&
68 return "Story graph data is not available for the active hack project";
75 void Draw(
bool* )
override {
85 manifest_ = backend->ResolveManifest(project);
86 }
else if (project && project->hack_manifest.loaded()) {
92 ImGui::TextDisabled(
"No hack project loaded");
94 "Open a project with a hack manifest to view story events.");
103 if (graph ==
nullptr || !graph->loaded()) {
104 ImGui::TextDisabled(
"No story events data available");
109 if (ImGui::Button(
"Reset View")) {
115 ImGui::SliderFloat(
"Zoom", &
zoom_, 0.3f, 2.0f,
"%.1f");
117 ImGui::Text(
"Nodes: %zu Edges: %zu", graph->nodes().size(),
118 graph->edges().size());
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());
129 ImGui::TextDisabled(
"No SRAM loaded");
133 if (ImGui::SmallButton(
"Import .srm...")) {
137 if (ImGui::SmallButton(
"Clear SRAM")) {
145 ImGui::TextDisabled(
"SRM: %s", p.filename().string().c_str());
146 if (ImGui::IsItemHovered()) {
152 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f),
"SRM error: %s",
164 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
165 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
169 canvas_size.x -= sidebar_width;
171 if (canvas_size.x < 100 || canvas_size.y < 100)
174 ImGui::InvisibleButton(
175 "story_canvas", canvas_size,
176 ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight);
178 bool is_hovered = ImGui::IsItemHovered();
179 bool is_active = ImGui::IsItemActive();
182 if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
183 ImVec2 delta = ImGui::GetIO().MouseDelta;
190 float wheel = ImGui::GetIO().MouseWheel;
192 zoom_ *= (wheel > 0) ? 1.1f : 0.9f;
200 ImDrawList* draw_list = ImGui::GetWindowDrawList();
203 draw_list->PushClipRect(
205 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
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_;
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)
224 cy + from_node->pos_y *
zoom_);
226 cy + to_node->pos_y *
zoom_);
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);
233 draw_list->AddBezierCubic(p1, cp1, cp2, p2, IM_COL32(150, 150, 150, 180),
237 ImVec2 dir(p2.x - cp2.x, p2.y - cp2.y);
238 float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
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));
253 ImVec2 mouse_pos = ImGui::GetIO().MousePos;
255 for (
const auto& node : graph->nodes()) {
265 ImVec2 node_min(nx, ny);
266 ImVec2 node_max(nx + nw, ny + nh);
271 const bool query_match =
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));
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,
283 float font_size = 11.0f *
zoom_;
284 if (font_size >= 6.0f) {
286 draw_list->AddText(
nullptr, font_size,
288 IM_COL32(200, 200, 200, 255), node.id.c_str());
290 std::string display_name = node.name;
291 if (display_name.length() > 25) {
292 display_name = display_name.substr(0, 22) +
"...";
294 draw_list->AddText(
nullptr, font_size,
296 IM_COL32(255, 255, 255, 255), display_name.c_str());
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) {
308 draw_list->PopClipRect();
326 return IM_COL32(40, 120, 40, 220);
328 return IM_COL32(180, 160, 40, 220);
330 return IM_COL32(160, 40, 40, 220);
333 return IM_COL32(60, 60, 60, 220);
342 ImGui::BeginChild(
"node_detail", ImVec2(240, 0), ImGuiChildFlags_Borders);
344 ImGui::TextWrapped(
"%s", node->name.c_str());
345 ImGui::TextDisabled(
"%s", node->id.c_str());
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());
354 ImGui::BulletText(
"%s", flag.name.c_str());
359 if (!node->locations.empty()) {
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));
366 ImGui::BulletText(
"%s", loc.name.c_str());
370 if (ImGui::SmallButton(
"Room")) {
376 if (ImGui::SmallButton(
"Map")) {
381 if (!loc.room_id.empty() || !loc.overworld_id.empty() ||
382 !loc.entrance_id.empty()) {
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());
394 if (!node->text_ids.empty()) {
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));
401 ImGui::BulletText(
"%s", tid.c_str());
403 if (ImGui::SmallButton(
"Open")) {
409 if (ImGui::SmallButton(
"Copy")) {
410 ImGui::SetClipboardText(tid.c_str());
417 if (!node->scripts.empty()) {
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());
425 if (ImGui::SmallButton(
"Open")) {
429 if (ImGui::SmallButton(
"Copy")) {
430 ImGui::SetClipboardText(script.c_str());
436 if (!node->notes.empty()) {
438 ImGui::TextWrapped(
"Notes: %s", node->notes.c_str());
446 size_t start = input.find_first_not_of(
" \t\r\n");
447 if (start == std::string::npos)
449 size_t end = input.find_last_not_of(
" \t\r\n");
450 std::string trimmed = input.substr(start, end - start + 1);
454 int value = std::stoi(trimmed, &idx, 0);
455 if (idx != trimmed.size())
490 const auto prog_opt =
494 if (!prog_opt.has_value())
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);
509 ImGui::Text(
"Filter");
511 ImGui::SetNextItemWidth(260.0f);
512 if (ImGui::InputTextWithHint(
"##story_graph_filter",
513 "Search id/name/text/script/flag/room...",
518 if (ImGui::SmallButton(
"Clear")) {
532 bool toggles_changed =
false;
537 toggles_changed |= ImGui::Checkbox(
"Locked", &
show_locked_);
539 toggles_changed |= ImGui::Checkbox(
"Blocked", &
show_blocked_);
540 if (toggles_changed) {
545 static uint8_t
StatusMask(
bool completed,
bool available,
bool locked,
576 const size_t node_count = graph.
nodes().size();
601 for (
const auto& node : graph.
nodes()) {
602 const bool query_match =
624 {
"SRAM (.srm)",
"srm"},
628 std::string file_path =
630 if (file_path.empty()) {
638 if (!state_or.ok()) {
644 backend->SetProgressionState(*
manifest_, *state_or);
659 backend->ClearProgressionState(*
manifest_);
670 if (ImGui::SmallButton(
"Sync Mesen")) {
674 if (ImGui::IsItemHovered()) {
675 ImGui::SetTooltip(
"Read Oracle SRAM directly from connected Mesen2");
682 ImGui::SetNextItemWidth(70.0f);
683 ImGui::SliderFloat(
"##StoryGraphLiveInterval",
689 ImGui::TextDisabled(
"Mesen: connected");
691 ImGui::TextDisabled(
"Mesen: disconnected");
696 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f),
"Live sync error");
697 if (ImGui::IsItemHovered()) {
731 if (event.
type ==
"frame_complete" ||
732 event.
type ==
"breakpoint_hit" || event.
type ==
"all") {
733 live_refresh_pending_.store(true);
742 const double now = ImGui::GetTime();
748 auto status =
live_client_->Subscribe({
"frame_complete",
"breakpoint_hit"});
766 const double now = ImGui::GetTime();
786 if (backend ==
nullptr) {
791 auto state_or = backend->ReadProgressionStateFromLiveSram(*
live_client_);
792 if (!state_or.ok()) {
797 backend->SetProgressionState(*
manifest_, *state_or);
830 return backend->GetStoryGraph(*
manifest_);
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
Interactive node graph of Oracle narrative progression.
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
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
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