yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
progression_dashboard_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_ORACLE_PANELS_PROGRESSION_DASHBOARD_PANEL_H
2#define YAZE_APP_EDITOR_ORACLE_PANELS_PROGRESSION_DASHBOARD_PANEL_H
3
4#include <atomic>
5#include <cstdio>
6#include <filesystem>
7#include <memory>
8#include <string>
9#include <vector>
10
15#include "app/gui/core/icons.h"
17#include "core/hack_manifest.h"
20#include "core/project.h"
21#include "imgui/imgui.h"
22#include "util/file_util.h"
23
24namespace yaze::editor {
25
34class ProgressionDashboardPanel : public WindowContent {
35 public:
37
38 std::string GetId() const override { return "oracle.progression_dashboard"; }
39 std::string GetDisplayName() const override {
40 return "Game State Dashboard";
41 }
42 std::string GetIcon() const override { return ICON_MD_DASHBOARD; }
43 std::string GetEditorCategory() const override { return "Agent"; }
44 std::string GetWorkflowGroup() const override { return "Game State"; }
45 std::string GetWorkflowDescription() const override {
46 return "Inspect progression, SRAM-derived flags, and live game state";
47 }
48 bool IsEnabled() const override {
50 auto* backend = GetProgressionBackend();
51 return project != nullptr && project->project_opened() && backend != nullptr;
52 }
53 std::string GetDisabledTooltip() const override {
54 return "Progression tools are not available for the active hack project";
55 }
59 float GetPreferredWidth() const override { return 400.0f; }
60
61 void Draw(bool* p_open) override {
62 (void)p_open;
63
64 if (!IsEnabled()) {
65 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
66 return;
67 }
68
72
73 // Lazily resolve the manifest from the project context.
74 if (!manifest_) {
76 if (auto* backend = GetWorkflowBackend()) {
77 manifest_ = backend->ResolveManifest(project);
78 } else if (project && project->hack_manifest.loaded()) {
79 manifest_ = &project->hack_manifest;
80 }
81 }
82
84 ImGui::Separator();
85
87 ImGui::Separator();
89 ImGui::Separator();
91 ImGui::Separator();
93 ImGui::Separator();
95
97 }
98
99 private:
102 return a.crystal_bitfield == b.crystal_bitfield &&
103 a.game_state == b.game_state && a.oosprog == b.oosprog &&
104 a.oosprog2 == b.oosprog2 && a.side_quest == b.side_quest &&
105 a.pendants == b.pendants;
106 }
107
109 ImGui::Text("SRAM (.srm)");
110
112 options.filters = {
113 {"SRAM (.srm)", "srm"},
114 {"All Files", "*"},
115 };
116
117 if (ImGui::Button("Import...")) {
118 std::string file_path =
120 if (!file_path.empty()) {
121 auto state_or = GetProgressionBackend()
123 file_path)
125 if (state_or.ok()) {
126 state_ = *state_or;
127 game_state_slider_ = static_cast<int>(state_.game_state);
128 loaded_srm_path_ = file_path;
129 last_srm_error_.clear();
130 } else {
131 last_srm_error_ = std::string(state_or.status().message());
132 }
133 }
134 }
135
136 ImGui::SameLine();
137 if (ImGui::Button("Clear")) {
140 loaded_srm_path_.clear();
141 last_srm_error_.clear();
142 if (manifest_) {
143 if (auto* backend = GetProgressionBackend()) {
144 backend->ClearProgressionState(*manifest_);
145 } else {
147 }
148 }
149 }
150
151 if (!loaded_srm_path_.empty()) {
152 const std::filesystem::path p(loaded_srm_path_);
153 ImGui::TextDisabled("Loaded: %s", p.filename().string().c_str());
154 if (ImGui::IsItemHovered()) {
155 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
156 }
157 } else {
158 ImGui::TextDisabled("Loaded: (none)");
159 }
160
161 if (!last_srm_error_.empty()) {
162 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Error: %s",
163 last_srm_error_.c_str());
164 }
165
167 }
168
170 const bool connected = live_client_ && live_client_->IsConnected();
171
172 ImGui::Spacing();
173 ImGui::Text("Live SRAM (Mesen2)");
174 ImGui::SameLine();
175 if (connected) {
176 ImGui::TextColored(ImVec4(0.25f, 0.85f, 0.35f, 1.0f), "Connected");
177 } else {
178 ImGui::TextDisabled("Disconnected");
179 }
180
181 if (ImGui::SmallButton("Sync from Mesen")) {
182 live_refresh_pending_.store(false);
184 }
185 ImGui::SameLine();
186 ImGui::Checkbox("Auto (event-driven)", &live_sync_enabled_);
187 if (live_sync_enabled_) {
188 ImGui::SameLine();
189 ImGui::SetNextItemWidth(70.0f);
190 ImGui::SliderFloat("##LiveRefreshInterval", &live_refresh_interval_seconds_,
191 0.05f, 0.5f, "%.2fs");
192 }
193
194 if (!connected) {
195 ImGui::TextDisabled("Use a Mesen panel to connect (shared client).");
196 }
197
198 if (!live_sync_error_.empty()) {
199 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), "Live sync: %s",
200 live_sync_error_.c_str());
201 }
202 }
203
205 if (!manifest_ || !manifest_->loaded()) {
206 return;
207 }
208
209 const auto existing = manifest_->oracle_progression_state();
210 if (auto* backend = GetProgressionBackend()) {
211 const auto backend_existing = backend->GetProgressionState(*manifest_);
212 if (backend_existing.has_value() && StatesEqual(*backend_existing, state_)) {
213 return;
214 }
215 backend->SetProgressionState(*manifest_, state_);
216 return;
217 }
218
219 if (existing.has_value() && StatesEqual(*existing, state_)) {
220 return;
221 }
223 }
224
226 ImGui::Text("Crystal Tracker");
227 ImGui::Spacing();
228
229 float item_width = 44.0f;
230
231 for (int d = 1; d <= 7; ++d) {
233 bool complete = (state_.crystal_bitfield & mask) != 0;
234
235 ImVec4 color =
236 complete ? ImVec4(0.2f, 0.8f, 0.3f, 1.0f) // green
237 : ImVec4(0.3f, 0.3f, 0.3f, 0.6f); // gray
238
239 {
240 gui::StyleColorGuard crystal_guard(
241 {{ImGuiCol_Button, color},
242 {ImGuiCol_ButtonHovered,
243 ImVec4(color.x + 0.1f, color.y + 0.1f, color.z + 0.1f, 1.0f)}});
244
245 char label[8];
246 snprintf(label, sizeof(label), "D%d", d);
247 if (ImGui::Button(label, ImVec2(item_width, 36.0f))) {
248 // Toggle crystal bit for testing
249 state_.crystal_bitfield ^= mask;
250 }
251 }
252
253 if (d < 7) ImGui::SameLine();
254 }
255
256 ImGui::Text("Crystals: %d / 7", state_.GetCrystalCount());
257 }
258
260 ImGui::Text("Game State");
261 ImGui::Spacing();
262
263 // Phase labels
264 const char* phases[] = {"Start", "Loom Beach", "Kydrog Complete",
265 "Farore Rescued"};
266 int phase_count = 4;
267
268 float bar_width = ImGui::GetContentRegionAvail().x;
269 float segment = bar_width / static_cast<float>(phase_count);
270
271 ImVec2 bar_pos = ImGui::GetCursorScreenPos();
272 ImDrawList* draw_list = ImGui::GetWindowDrawList();
273
274 for (int i = 0; i < phase_count; ++i) {
275 ImVec2 seg_min(bar_pos.x + segment * i, bar_pos.y);
276 ImVec2 seg_max(bar_pos.x + segment * (i + 1), bar_pos.y + 24.0f);
277
278 ImU32 fill = (i <= state_.game_state)
279 ? IM_COL32(60, 140, 200, 220)
280 : IM_COL32(50, 50, 50, 180);
281
282 draw_list->AddRectFilled(seg_min, seg_max, fill, 3.0f);
283 draw_list->AddRect(seg_min, seg_max, IM_COL32(80, 80, 80, 255), 3.0f);
284
285 // Label
286 ImVec2 text_pos(seg_min.x + 4, seg_min.y + 4);
287 draw_list->AddText(text_pos, IM_COL32(220, 220, 220, 255), phases[i]);
288 }
289
290 ImGui::Dummy(ImVec2(0, 30));
291 ImGui::Text("Phase: %s", state_.GetGameStateName().c_str());
292 }
293
295 ImGui::Text("Dungeon Completion");
296 ImGui::Spacing();
297
298 struct DungeonInfo {
299 const char* label;
300 int number; // 0 means special (FOS/SOP/SOW)
301 };
302
303 DungeonInfo dungeons[] = {
304 {"D1 Mushroom Grotto", 1}, {"D2 Tail Palace", 2},
305 {"D3 Kalyxo Castle", 3}, {"D4 Zora Temple", 4},
306 {"D5 Glacia Estate", 5}, {"D6 Goron Mines", 6},
307 {"D7 Dragon Ship", 7}, {"FOS Fortress", 0},
308 {"SOP Shrine of Power", 0}, {"SOW Shrine of Wisdom", 0},
309 };
310
311 ImGui::Columns(2, "dungeon_grid", false);
312 for (const auto& dungeon : dungeons) {
313 bool complete = false;
314 if (dungeon.number >= 1 && dungeon.number <= 7) {
315 complete = state_.IsDungeonComplete(dungeon.number);
316 }
317
318 ImVec4 color =
319 complete ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f) // green
320 : ImVec4(0.25f, 0.25f, 0.25f, 1.0f); // dark
321
322 {
323 gui::StyleColorGuard header_guard(ImGuiCol_Header, color);
324 ImGui::Selectable(dungeon.label, complete,
325 ImGuiSelectableFlags_Disabled);
326 }
327 ImGui::NextColumn();
328 }
329 ImGui::Columns(1);
330 }
331
333 if (!ImGui::TreeNode("Story Flags")) return;
334
335 // OOSPROG bits
336 ImGui::Text("OOSPROG ($7EF3D6): 0x%02X", state_.oosprog);
338
339 ImGui::Spacing();
340
341 // OOSPROG2 bits
342 ImGui::Text("OOSPROG2 ($7EF3C6): 0x%02X", state_.oosprog2);
344
345 ImGui::Spacing();
346
347 // Side quest
348 ImGui::Text("SideQuest ($7EF3D7): 0x%02X", state_.side_quest);
349
350 ImGui::TreePop();
351 }
352
353 static void DrawBitGrid(const char* id_prefix, uint8_t value,
354 const char* const* labels) {
355 for (int bit = 0; bit < 8; ++bit) {
356 bool set = (value & (1 << bit)) != 0;
357 ImVec4 color = set ? ImVec4(0.3f, 0.7f, 0.3f, 1.0f)
358 : ImVec4(0.2f, 0.2f, 0.2f, 0.6f);
359
360 char buf[64];
361 snprintf(buf, sizeof(buf), "%s##%s_%d", labels[bit], id_prefix, bit);
362
363 {
364 gui::StyleColorGuard bit_guard(ImGuiCol_Button, color);
365 ImGui::SmallButton(buf);
366 }
367 if (bit < 7) ImGui::SameLine();
368 }
369 }
370
372 if (!ImGui::TreeNode("Manual Controls")) return;
373
374 ImGui::SliderInt("Game State", &game_state_slider_, 0, 3);
376 state_.game_state = static_cast<uint8_t>(game_state_slider_);
377 }
378
379 int crystal_int = state_.crystal_bitfield;
380 if (ImGui::SliderInt("Crystal Bits", &crystal_int, 0, 127)) {
381 state_.crystal_bitfield = static_cast<uint8_t>(crystal_int);
382 }
383
384 if (ImGui::Button("Clear All")) {
387 }
388 ImGui::SameLine();
389 if (ImGui::Button("Complete All")) {
391 state_.game_state = 3;
393 }
394
395 ImGui::TreePop();
396 }
397
400 if (client == live_client_) {
401 return;
402 }
403
405 live_client_ = std::move(client);
407 live_sync_error_.clear();
408
409 if (live_client_ && live_client_->IsConnected()) {
410 live_refresh_pending_.store(true);
411 }
412 }
413
415 if (!live_sync_enabled_) {
416 return;
417 }
418 if (!live_client_ || !live_client_->IsConnected()) {
420 return;
421 }
422
423 if (live_listener_id_ == 0) {
425 live_client_->AddEventListener([this](const emu::mesen::MesenEvent& event) {
426 if (event.type == "frame_complete" ||
427 event.type == "breakpoint_hit" || event.type == "all") {
428 live_refresh_pending_.store(true);
429 }
430 });
431 }
432
434 return;
435 }
436
437 const double now = ImGui::GetTime();
438 if ((now - last_subscribe_attempt_time_) < 1.0) {
439 return;
440 }
442
443 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
444 if (!status.ok()) {
445 live_sync_error_ = std::string(status.message());
446 return;
447 }
448
450 live_sync_error_.clear();
451 live_refresh_pending_.store(true);
452 }
453
455 if (!live_sync_enabled_) {
456 return;
457 }
458 if (!live_refresh_pending_.load()) {
459 return;
460 }
461 const double now = ImGui::GetTime();
463 return;
464 }
467 }
468 live_refresh_pending_.store(false);
469 }
470
472 if (!live_client_ || !live_client_->IsConnected()) {
473 live_sync_error_ = "Mesen client is not connected";
474 return false;
475 }
476
477 if (auto* backend = GetProgressionBackend()) {
478 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
479 if (!state_or.ok()) {
480 live_sync_error_ = std::string(state_or.status().message());
481 return false;
482 }
483 state_ = *state_or;
484 game_state_slider_ = static_cast<int>(state_.game_state);
485 loaded_srm_path_ = "Mesen2 Live";
486 last_srm_error_.clear();
487 live_sync_error_.clear();
488 return true;
489 }
490
491 constexpr uint32_t kBaseAddress = 0x7EF000;
492 constexpr uint16_t kStartOffset = core::OracleProgressionState::kPendantOffset;
493 constexpr uint16_t kEndOffset = core::OracleProgressionState::kSideQuestOffset;
494 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
495 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
496
497 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
498 if (!bytes_or.ok()) {
499 live_sync_error_ = std::string(bytes_or.status().message());
500 return false;
501 }
502 if (bytes_or->size() < kReadLength) {
503 live_sync_error_ = "SRAM read returned truncated data";
504 return false;
505 }
506
507 const auto read_byte = [&](uint16_t offset) -> uint8_t {
508 return (*bytes_or)[offset - kStartOffset];
509 };
510
518 game_state_slider_ = static_cast<int>(state_.game_state);
519
520 loaded_srm_path_ = "Mesen2 Live";
521 last_srm_error_.clear();
522 live_sync_error_.clear();
523 return true;
524 }
525
527 if (live_client_ && live_listener_id_ != 0) {
528 live_client_->RemoveEventListener(live_listener_id_);
529 }
532 }
533
537
541
543 int game_state_slider_ = 0;
544
545 core::HackManifest* manifest_ = nullptr;
546 std::string loaded_srm_path_;
547 std::string last_srm_error_;
548
549 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
551 bool live_sync_enabled_ = true;
552 bool live_subscription_active_ = false;
553 std::atomic<bool> live_refresh_pending_{false};
554 float live_refresh_interval_seconds_ = 0.10f;
555 double last_live_refresh_time_ = 0.0;
556 double last_subscribe_attempt_time_ = 0.0;
557 std::string live_sync_error_;
558
559 // Bit labels for flag grids
560 static constexpr const char* oosprog_labels_[8] = {
561 "Bit0", "HallOfSecrets", "PendantQuest", "Bit3",
562 "ElderMet", "Bit5", "Bit6", "FortressComplete",
563 };
564
565 static constexpr const char* oosprog2_labels_[8] = {
566 "Bit0", "Bit1", "KydrogEncounter", "Bit3",
567 "DekuSoulFreed", "BookOfSecrets", "Bit6", "Bit7",
568 };
569};
570
571} // namespace yaze::editor
572
573#endif // YAZE_APP_EDITOR_ORACLE_PANELS_PROGRESSION_DASHBOARD_PANEL_H
Loads and queries the hack manifest JSON for yaze-ASM integration.
std::optional< OracleProgressionState > oracle_progression_state() const
bool loaded() const
Check if the manifest has been loaded.
void SetOracleProgressionState(const OracleProgressionState &state)
static void DrawBitGrid(const char *id_prefix, uint8_t value, const char *const *labels)
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetWorkflowDescription() const override
Optional workflow description for menus/command palette.
static bool StatesEqual(const core::OracleProgressionState &a, const core::OracleProgressionState &b)
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
std::shared_ptr< emu::mesen::MesenSocketClient > live_client_
WindowLifecycle GetWindowLifecycle() const override
Get the lifecycle category for this window.
workflow::HackWorkflowBackend * GetWorkflowBackend() const
std::string GetDisabledTooltip() const override
Get tooltip text when panel is disabled.
void Draw(bool *p_open) override
Draw the panel content.
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
std::string GetId() const override
Unique identifier for this panel.
workflow::ProgressionCapability * GetProgressionBackend() const
static constexpr const char * oosprog2_labels_[8]
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
bool IsEnabled() const override
Check if this panel is currently enabled.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
virtual absl::StatusOr< core::OracleProgressionState > LoadProgressionStateFromFile(const std::string &filepath) const =0
static std::shared_ptr< MesenSocketClient > & GetClient()
RAII guard for ImGui style colors.
Definition style_guard.h:27
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define ICON_MD_DASHBOARD
Definition icons.h:517
absl::StatusOr< OracleProgressionState > LoadOracleProgressionFromSrmFile(const std::string &srm_path)
::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.
Oracle of Secrets game progression state parsed from SRAM.
static constexpr uint16_t kSideQuestOffset
static uint8_t GetCrystalMask(int dungeon_number)
Get the crystal bitmask for a dungeon number (1-7).
static constexpr uint16_t kPendantOffset
static constexpr uint16_t kGameStateOffset
int GetCrystalCount() const
Count completed dungeons using popcount on crystal bitfield.
static constexpr uint16_t kOosProgOffset
bool IsDungeonComplete(int dungeon_number) const
Check if a specific dungeon is complete.
static constexpr uint16_t kOosProg2Offset
std::string GetGameStateName() const
Get human-readable name for the current game state.
static constexpr uint16_t kCrystalOffset
Event from Mesen2 subscription.
std::vector< FileDialogFilter > filters
Definition file_util.h:17