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#include "util/i18n/tr.h"
11
16#include "app/gui/core/icons.h"
18#include "core/hack_manifest.h"
21#include "core/project.h"
22#include "imgui/imgui.h"
23#include "util/file_util.h"
24
25namespace yaze::editor {
26
35class ProgressionDashboardPanel : public WindowContent {
36 public:
38
39 std::string GetId() const override { return "oracle.progression_dashboard"; }
40 std::string GetDisplayName() const override { return "Game State Dashboard"; }
41 std::string GetIcon() const override { return ICON_MD_DASHBOARD; }
42 std::string GetEditorCategory() const override { return "Agent"; }
46 std::string GetWorkflowGroup() const override { return "Game State"; }
47 std::string GetWorkflowDescription() const override {
48 return "Inspect progression, SRAM-derived flags, and live game state";
49 }
50 bool IsEnabled() const override {
52 auto* backend = GetProgressionBackend();
53 return project != nullptr && project->project_opened() &&
54 backend != nullptr;
55 }
56 std::string GetDisabledTooltip() const override {
57 return "Progression tools are not available for the active hack project";
58 }
62 float GetPreferredWidth() const override { return 400.0f; }
63
64 void Draw(bool* p_open) override {
65 (void)p_open;
66
67 if (!IsEnabled()) {
68 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
69 return;
70 }
71
75
76 // Lazily resolve the manifest from the project context.
77 if (!manifest_) {
79 if (auto* backend = GetWorkflowBackend()) {
80 manifest_ = backend->ResolveManifest(project);
81 } else if (project && project->hack_manifest.loaded()) {
82 manifest_ = &project->hack_manifest;
83 }
84 }
85
87 ImGui::Separator();
88
90 ImGui::Separator();
92 ImGui::Separator();
94 ImGui::Separator();
96 ImGui::Separator();
98
100 }
101
102 private:
105 return a.crystal_bitfield == b.crystal_bitfield &&
106 a.game_state == b.game_state && a.oosprog == b.oosprog &&
107 a.oosprog2 == b.oosprog2 && a.side_quest == b.side_quest &&
108 a.pendants == b.pendants;
109 }
110
112 ImGui::Text(tr("SRAM (.srm)"));
113
115 options.filters = {
116 {"SRAM (.srm)", "srm"},
117 {"All Files", "*"},
118 };
119
120 if (ImGui::Button(tr("Import..."))) {
121 std::string file_path =
123 if (!file_path.empty()) {
124 auto state_or =
127 file_path)
129 if (state_or.ok()) {
130 state_ = *state_or;
131 game_state_slider_ = static_cast<int>(state_.game_state);
132 loaded_srm_path_ = file_path;
133 last_srm_error_.clear();
134 } else {
135 last_srm_error_ = std::string(state_or.status().message());
136 }
137 }
138 }
139
140 ImGui::SameLine();
141 if (ImGui::Button(tr("Clear"))) {
144 loaded_srm_path_.clear();
145 last_srm_error_.clear();
146 if (manifest_) {
147 if (auto* backend = GetProgressionBackend()) {
148 backend->ClearProgressionState(*manifest_);
149 } else {
151 }
152 }
153 }
154
155 if (!loaded_srm_path_.empty()) {
156 const std::filesystem::path p(loaded_srm_path_);
157 ImGui::TextDisabled(tr("Loaded: %s"), p.filename().string().c_str());
158 if (ImGui::IsItemHovered()) {
159 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
160 }
161 } else {
162 ImGui::TextDisabled(tr("Loaded: (none)"));
163 }
164
165 if (!last_srm_error_.empty()) {
166 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), tr("Error: %s"),
167 last_srm_error_.c_str());
168 }
169
171 }
172
174 const bool connected = live_client_ && live_client_->IsConnected();
175
176 ImGui::Spacing();
177 ImGui::Text(tr("Live SRAM (Mesen2)"));
178 ImGui::SameLine();
179 if (connected) {
180 ImGui::TextColored(ImVec4(0.25f, 0.85f, 0.35f, 1.0f), tr("Connected"));
181 } else {
182 ImGui::TextDisabled(tr("Disconnected"));
183 }
184
185 if (ImGui::SmallButton(tr("Sync from Mesen"))) {
186 live_refresh_pending_.store(false);
188 }
189 ImGui::SameLine();
190 ImGui::Checkbox(tr("Auto (event-driven)"), &live_sync_enabled_);
191 if (live_sync_enabled_) {
192 ImGui::SameLine();
193 ImGui::SetNextItemWidth(70.0f);
194 ImGui::SliderFloat("##LiveRefreshInterval",
195 &live_refresh_interval_seconds_, 0.05f, 0.5f, "%.2fs");
196 }
197
198 if (!connected) {
199 ImGui::TextDisabled(tr("Use a Mesen panel to connect (shared client)."));
200 }
201
202 if (!live_sync_error_.empty()) {
203 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), tr("Live sync: %s"),
204 live_sync_error_.c_str());
205 }
206 }
207
209 if (!manifest_ || !manifest_->loaded()) {
210 return;
211 }
212
213 const auto existing = manifest_->oracle_progression_state();
214 if (auto* backend = GetProgressionBackend()) {
215 const auto backend_existing = backend->GetProgressionState(*manifest_);
216 if (backend_existing.has_value() &&
217 StatesEqual(*backend_existing, state_)) {
218 return;
219 }
220 backend->SetProgressionState(*manifest_, state_);
221 return;
222 }
223
224 if (existing.has_value() && StatesEqual(*existing, state_)) {
225 return;
226 }
228 }
229
231 ImGui::Text(tr("Crystal Tracker"));
232 ImGui::Spacing();
233
234 float item_width = 44.0f;
235
236 for (int d = 1; d <= 7; ++d) {
238 bool complete = (state_.crystal_bitfield & mask) != 0;
239
240 ImVec4 color = complete ? ImVec4(0.2f, 0.8f, 0.3f, 1.0f) // green
241 : ImVec4(0.3f, 0.3f, 0.3f, 0.6f); // gray
242
243 {
244 gui::StyleColorGuard crystal_guard(
245 {{ImGuiCol_Button, color},
246 {ImGuiCol_ButtonHovered,
247 ImVec4(color.x + 0.1f, color.y + 0.1f, color.z + 0.1f, 1.0f)}});
248
249 char label[8];
250 snprintf(label, sizeof(label), "D%d", d);
251 if (ImGui::Button(label, ImVec2(item_width, 36.0f))) {
252 // Toggle crystal bit for testing
253 state_.crystal_bitfield ^= mask;
254 }
255 }
256
257 if (d < 7)
258 ImGui::SameLine();
259 }
260
261 ImGui::Text(tr("Crystals: %d / 7"), state_.GetCrystalCount());
262 }
263
265 ImGui::Text(tr("Game State"));
266 ImGui::Spacing();
267
268 // Phase labels
269 const char* phases[] = {"Start", "Loom Beach", "Kydrog Complete",
270 "Farore Rescued"};
271 int phase_count = 4;
272
273 float bar_width = ImGui::GetContentRegionAvail().x;
274 float segment = bar_width / static_cast<float>(phase_count);
275
276 ImVec2 bar_pos = ImGui::GetCursorScreenPos();
277 ImDrawList* draw_list = ImGui::GetWindowDrawList();
278
279 for (int i = 0; i < phase_count; ++i) {
280 ImVec2 seg_min(bar_pos.x + segment * i, bar_pos.y);
281 ImVec2 seg_max(bar_pos.x + segment * (i + 1), bar_pos.y + 24.0f);
282
283 ImU32 fill = (i <= state_.game_state) ? IM_COL32(60, 140, 200, 220)
284 : IM_COL32(50, 50, 50, 180);
285
286 draw_list->AddRectFilled(seg_min, seg_max, fill, 3.0f);
287 draw_list->AddRect(seg_min, seg_max, IM_COL32(80, 80, 80, 255), 3.0f);
288
289 // Label
290 ImVec2 text_pos(seg_min.x + 4, seg_min.y + 4);
291 draw_list->AddText(text_pos, IM_COL32(220, 220, 220, 255), phases[i]);
292 }
293
294 ImGui::Dummy(ImVec2(0, 30));
295 ImGui::Text(tr("Phase: %s"), state_.GetGameStateName().c_str());
296 }
297
299 ImGui::Text(tr("Dungeon Completion"));
300 ImGui::Spacing();
301
302 struct DungeonInfo {
303 const char* label;
304 int number; // 0 means special (FOS/SOP/SOW)
305 };
306
307 DungeonInfo dungeons[] = {
308 {"D1 Mushroom Grotto", 1}, {"D2 Tail Palace", 2},
309 {"D3 Kalyxo Castle", 3}, {"D4 Zora Temple", 4},
310 {"D5 Glacia Estate", 5}, {"D6 Goron Mines", 6},
311 {"D7 Dragon Ship", 7}, {"FOS Fortress", 0},
312 {"SOP Shrine of Power", 0}, {"SOW Shrine of Wisdom", 0},
313 };
314
315 ImGui::Columns(2, "dungeon_grid", false);
316 for (const auto& dungeon : dungeons) {
317 bool complete = false;
318 if (dungeon.number >= 1 && dungeon.number <= 7) {
319 complete = state_.IsDungeonComplete(dungeon.number);
320 }
321
322 ImVec4 color = complete ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f) // green
323 : ImVec4(0.25f, 0.25f, 0.25f, 1.0f); // dark
324
325 {
326 gui::StyleColorGuard header_guard(ImGuiCol_Header, color);
327 ImGui::Selectable(dungeon.label, complete,
328 ImGuiSelectableFlags_Disabled);
329 }
330 ImGui::NextColumn();
331 }
332 ImGui::Columns(1);
333 }
334
336 if (!ImGui::TreeNode("Story Flags"))
337 return;
338
339 // OOSPROG bits
340 ImGui::Text(tr("OOSPROG ($7EF3D6): 0x%02X"), state_.oosprog);
342
343 ImGui::Spacing();
344
345 // OOSPROG2 bits
346 ImGui::Text(tr("OOSPROG2 ($7EF3C6): 0x%02X"), state_.oosprog2);
348
349 ImGui::Spacing();
350
351 // Side quest
352 ImGui::Text(tr("SideQuest ($7EF3D7): 0x%02X"), state_.side_quest);
353
354 ImGui::TreePop();
355 }
356
357 static void DrawBitGrid(const char* id_prefix, uint8_t value,
358 const char* const* labels) {
359 for (int bit = 0; bit < 8; ++bit) {
360 bool set = (value & (1 << bit)) != 0;
361 ImVec4 color =
362 set ? ImVec4(0.3f, 0.7f, 0.3f, 1.0f) : ImVec4(0.2f, 0.2f, 0.2f, 0.6f);
363
364 char buf[64];
365 snprintf(buf, sizeof(buf), "%s##%s_%d", labels[bit], id_prefix, bit);
366
367 {
368 gui::StyleColorGuard bit_guard(ImGuiCol_Button, color);
369 ImGui::SmallButton(buf);
370 }
371 if (bit < 7)
372 ImGui::SameLine();
373 }
374 }
375
377 if (!ImGui::TreeNode("Manual Controls"))
378 return;
379
380 ImGui::SliderInt(tr("Game State"), &game_state_slider_, 0, 3);
382 state_.game_state = static_cast<uint8_t>(game_state_slider_);
383 }
384
385 int crystal_int = state_.crystal_bitfield;
386 if (ImGui::SliderInt(tr("Crystal Bits"), &crystal_int, 0, 127)) {
387 state_.crystal_bitfield = static_cast<uint8_t>(crystal_int);
388 }
389
390 if (ImGui::Button(tr("Clear All"))) {
393 }
394 ImGui::SameLine();
395 if (ImGui::Button(tr("Complete All"))) {
397 state_.game_state = 3;
399 }
400
401 ImGui::TreePop();
402 }
403
406 if (client == live_client_) {
407 return;
408 }
409
411 live_client_ = std::move(client);
413 live_sync_error_.clear();
414
415 if (live_client_ && live_client_->IsConnected()) {
416 live_refresh_pending_.store(true);
417 }
418 }
419
421 if (!live_sync_enabled_) {
422 return;
423 }
424 if (!live_client_ || !live_client_->IsConnected()) {
426 return;
427 }
428
429 if (live_listener_id_ == 0) {
430 live_listener_id_ = live_client_->AddEventListener(
431 [this](const emu::mesen::MesenEvent& event) {
432 if (event.type == "frame_complete" ||
433 event.type == "breakpoint_hit" || event.type == "all") {
434 live_refresh_pending_.store(true);
435 }
436 });
437 }
438
440 return;
441 }
442
443 const double now = ImGui::GetTime();
444 if ((now - last_subscribe_attempt_time_) < 1.0) {
445 return;
446 }
448
449 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
450 if (!status.ok()) {
451 live_sync_error_ = std::string(status.message());
452 return;
453 }
454
456 live_sync_error_.clear();
457 live_refresh_pending_.store(true);
458 }
459
461 if (!live_sync_enabled_) {
462 return;
463 }
464 if (!live_refresh_pending_.load()) {
465 return;
466 }
467 const double now = ImGui::GetTime();
469 return;
470 }
473 }
474 live_refresh_pending_.store(false);
475 }
476
478 if (!live_client_ || !live_client_->IsConnected()) {
479 live_sync_error_ = "Mesen client is not connected";
480 return false;
481 }
482
483 if (auto* backend = GetProgressionBackend()) {
484 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
485 if (!state_or.ok()) {
486 live_sync_error_ = std::string(state_or.status().message());
487 return false;
488 }
489 state_ = *state_or;
490 game_state_slider_ = static_cast<int>(state_.game_state);
491 loaded_srm_path_ = "Mesen2 Live";
492 last_srm_error_.clear();
493 live_sync_error_.clear();
494 return true;
495 }
496
497 constexpr uint32_t kBaseAddress = 0x7EF000;
498 constexpr uint16_t kStartOffset =
500 constexpr uint16_t kEndOffset =
502 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
503 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
504
505 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
506 if (!bytes_or.ok()) {
507 live_sync_error_ = std::string(bytes_or.status().message());
508 return false;
509 }
510 if (bytes_or->size() < kReadLength) {
511 live_sync_error_ = "SRAM read returned truncated data";
512 return false;
513 }
514
515 const auto read_byte = [&](uint16_t offset) -> uint8_t {
516 return (*bytes_or)[offset - kStartOffset];
517 };
518
528 game_state_slider_ = static_cast<int>(state_.game_state);
529
530 loaded_srm_path_ = "Mesen2 Live";
531 last_srm_error_.clear();
532 live_sync_error_.clear();
533 return true;
534 }
535
537 if (live_client_ && live_listener_id_ != 0) {
538 live_client_->RemoveEventListener(live_listener_id_);
539 }
542 }
543
547
551
553 int game_state_slider_ = 0;
554
555 core::HackManifest* manifest_ = nullptr;
556 std::string loaded_srm_path_;
557 std::string last_srm_error_;
558
559 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
561 bool live_sync_enabled_ = true;
562 bool live_subscription_active_ = false;
563 std::atomic<bool> live_refresh_pending_{false};
564 float live_refresh_interval_seconds_ = 0.10f;
565 double last_live_refresh_time_ = 0.0;
566 double last_subscribe_attempt_time_ = 0.0;
567 std::string live_sync_error_;
568
569 // Bit labels for flag grids
570 static constexpr const char* oosprog_labels_[8] = {
571 "Bit0", "HallOfSecrets", "PendantQuest", "Bit3",
572 "ElderMet", "Bit5", "Bit6", "FortressComplete",
573 };
574
575 static constexpr const char* oosprog2_labels_[8] = {
576 "Bit0", "Bit1", "KydrogEncounter",
577 "Bit3", "DekuSoulFreed", "BookOfSecrets",
578 "Bit6", "Bit7",
579 };
580};
581
582} // namespace yaze::editor
583
584#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]
WindowPresentationPolicy GetPresentationPolicy() const override
How this content is hosted and advertised by workspace chrome.
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
static WindowPresentationPolicy OptionalPopOut()
Event from Mesen2 subscription.
std::vector< FileDialogFilter > filters
Definition file_util.h:17