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_HACK_ORACLE_UI_PROGRESSION_DASHBOARD_PANEL_H
2#define YAZE_APP_EDITOR_HACK_ORACLE_UI_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
35 public:
37
38 std::string GetId() const override { return "oracle.progression_dashboard"; }
39 std::string GetDisplayName() const override { return "Game State Dashboard"; }
40 std::string GetIcon() const override { return ICON_MD_DASHBOARD; }
41 std::string GetEditorCategory() const override { return "Agent"; }
42 std::string GetWorkflowGroup() const override { return "Game State"; }
43 std::string GetWorkflowDescription() const override {
44 return "Inspect progression, SRAM-derived flags, and live game state";
45 }
46 bool IsEnabled() const override {
48 auto* backend = GetProgressionBackend();
49 return project != nullptr && project->project_opened() &&
50 backend != nullptr;
51 }
52 std::string GetDisabledTooltip() const override {
53 return "Progression tools are not available for the active hack project";
54 }
58 float GetPreferredWidth() const override { return 400.0f; }
59
60 void Draw(bool* p_open) override {
61 (void)p_open;
62
63 if (!IsEnabled()) {
64 ImGui::TextDisabled("%s", GetDisabledTooltip().c_str());
65 return;
66 }
67
71
72 // Lazily resolve the manifest from the project context.
73 if (!manifest_) {
75 if (auto* backend = GetWorkflowBackend()) {
76 manifest_ = backend->ResolveManifest(project);
77 } else if (project && project->hack_manifest.loaded()) {
78 manifest_ = &project->hack_manifest;
79 }
80 }
81
83 ImGui::Separator();
84
86 ImGui::Separator();
88 ImGui::Separator();
90 ImGui::Separator();
92 ImGui::Separator();
94
96 }
97
98 private:
101 return a.crystal_bitfield == b.crystal_bitfield &&
102 a.game_state == b.game_state && a.oosprog == b.oosprog &&
103 a.oosprog2 == b.oosprog2 && a.side_quest == b.side_quest &&
104 a.pendants == b.pendants;
105 }
106
108 ImGui::Text("SRAM (.srm)");
109
111 options.filters = {
112 {"SRAM (.srm)", "srm"},
113 {"All Files", "*"},
114 };
115
116 if (ImGui::Button("Import...")) {
117 std::string file_path =
119 if (!file_path.empty()) {
120 auto state_or =
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",
191 &live_refresh_interval_seconds_, 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() &&
213 StatesEqual(*backend_existing, state_)) {
214 return;
215 }
216 backend->SetProgressionState(*manifest_, state_);
217 return;
218 }
219
220 if (existing.has_value() && StatesEqual(*existing, state_)) {
221 return;
222 }
224 }
225
227 ImGui::Text("Crystal Tracker");
228 ImGui::Spacing();
229
230 float item_width = 44.0f;
231
232 for (int d = 1; d <= 7; ++d) {
234 bool complete = (state_.crystal_bitfield & mask) != 0;
235
236 ImVec4 color = 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)
254 ImGui::SameLine();
255 }
256
257 ImGui::Text("Crystals: %d / 7", state_.GetCrystalCount());
258 }
259
261 ImGui::Text("Game State");
262 ImGui::Spacing();
263
264 // Phase labels
265 const char* phases[] = {"Start", "Loom Beach", "Kydrog Complete",
266 "Farore Rescued"};
267 int phase_count = 4;
268
269 float bar_width = ImGui::GetContentRegionAvail().x;
270 float segment = bar_width / static_cast<float>(phase_count);
271
272 ImVec2 bar_pos = ImGui::GetCursorScreenPos();
273 ImDrawList* draw_list = ImGui::GetWindowDrawList();
274
275 for (int i = 0; i < phase_count; ++i) {
276 ImVec2 seg_min(bar_pos.x + segment * i, bar_pos.y);
277 ImVec2 seg_max(bar_pos.x + segment * (i + 1), bar_pos.y + 24.0f);
278
279 ImU32 fill = (i <= state_.game_state) ? 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 = complete ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f) // green
319 : ImVec4(0.25f, 0.25f, 0.25f, 1.0f); // dark
320
321 {
322 gui::StyleColorGuard header_guard(ImGuiCol_Header, color);
323 ImGui::Selectable(dungeon.label, complete,
324 ImGuiSelectableFlags_Disabled);
325 }
326 ImGui::NextColumn();
327 }
328 ImGui::Columns(1);
329 }
330
332 if (!ImGui::TreeNode("Story Flags"))
333 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 =
358 set ? ImVec4(0.3f, 0.7f, 0.3f, 1.0f) : 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)
368 ImGui::SameLine();
369 }
370 }
371
373 if (!ImGui::TreeNode("Manual Controls"))
374 return;
375
376 ImGui::SliderInt("Game State", &game_state_slider_, 0, 3);
378 state_.game_state = static_cast<uint8_t>(game_state_slider_);
379 }
380
381 int crystal_int = state_.crystal_bitfield;
382 if (ImGui::SliderInt("Crystal Bits", &crystal_int, 0, 127)) {
383 state_.crystal_bitfield = static_cast<uint8_t>(crystal_int);
384 }
385
386 if (ImGui::Button("Clear All")) {
389 }
390 ImGui::SameLine();
391 if (ImGui::Button("Complete All")) {
393 state_.game_state = 3;
395 }
396
397 ImGui::TreePop();
398 }
399
402 if (client == live_client_) {
403 return;
404 }
405
407 live_client_ = std::move(client);
409 live_sync_error_.clear();
410
411 if (live_client_ && live_client_->IsConnected()) {
412 live_refresh_pending_.store(true);
413 }
414 }
415
417 if (!live_sync_enabled_) {
418 return;
419 }
420 if (!live_client_ || !live_client_->IsConnected()) {
422 return;
423 }
424
425 if (live_listener_id_ == 0) {
426 live_listener_id_ = live_client_->AddEventListener(
427 [this](const emu::mesen::MesenEvent& event) {
428 if (event.type == "frame_complete" ||
429 event.type == "breakpoint_hit" || event.type == "all") {
430 live_refresh_pending_.store(true);
431 }
432 });
433 }
434
436 return;
437 }
438
439 const double now = ImGui::GetTime();
440 if ((now - last_subscribe_attempt_time_) < 1.0) {
441 return;
442 }
444
445 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
446 if (!status.ok()) {
447 live_sync_error_ = std::string(status.message());
448 return;
449 }
450
452 live_sync_error_.clear();
453 live_refresh_pending_.store(true);
454 }
455
457 if (!live_sync_enabled_) {
458 return;
459 }
460 if (!live_refresh_pending_.load()) {
461 return;
462 }
463 const double now = ImGui::GetTime();
465 return;
466 }
469 }
470 live_refresh_pending_.store(false);
471 }
472
474 if (!live_client_ || !live_client_->IsConnected()) {
475 live_sync_error_ = "Mesen client is not connected";
476 return false;
477 }
478
479 if (auto* backend = GetProgressionBackend()) {
480 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
481 if (!state_or.ok()) {
482 live_sync_error_ = std::string(state_or.status().message());
483 return false;
484 }
485 state_ = *state_or;
486 game_state_slider_ = static_cast<int>(state_.game_state);
487 loaded_srm_path_ = "Mesen2 Live";
488 last_srm_error_.clear();
489 live_sync_error_.clear();
490 return true;
491 }
492
493 constexpr uint32_t kBaseAddress = 0x7EF000;
494 constexpr uint16_t kStartOffset =
496 constexpr uint16_t kEndOffset =
498 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
499 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
500
501 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
502 if (!bytes_or.ok()) {
503 live_sync_error_ = std::string(bytes_or.status().message());
504 return false;
505 }
506 if (bytes_or->size() < kReadLength) {
507 live_sync_error_ = "SRAM read returned truncated data";
508 return false;
509 }
510
511 const auto read_byte = [&](uint16_t offset) -> uint8_t {
512 return (*bytes_or)[offset - kStartOffset];
513 };
514
524 game_state_slider_ = static_cast<int>(state_.game_state);
525
526 loaded_srm_path_ = "Mesen2 Live";
527 last_srm_error_.clear();
528 live_sync_error_.clear();
529 return true;
530 }
531
533 if (live_client_ && live_listener_id_ != 0) {
534 live_client_->RemoveEventListener(live_listener_id_);
535 }
538 }
539
543
547
550
552 std::string loaded_srm_path_;
553 std::string last_srm_error_;
554
555 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
559 std::atomic<bool> live_refresh_pending_{false};
563 std::string live_sync_error_;
564
565 // Bit labels for flag grids
566 static constexpr const char* oosprog_labels_[8] = {
567 "Bit0", "HallOfSecrets", "PendantQuest", "Bit3",
568 "ElderMet", "Bit5", "Bit6", "FortressComplete",
569 };
570
571 static constexpr const char* oosprog2_labels_[8] = {
572 "Bit0", "Bit1", "KydrogEncounter",
573 "Bit3", "DekuSoulFreed", "BookOfSecrets",
574 "Bit6", "Bit7",
575 };
576};
577
578} // namespace yaze::editor
579
580#endif // YAZE_APP_EDITOR_HACK_ORACLE_UI_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)
Visual dashboard of Oracle game state from SRAM data.
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.
Base interface for all logical window content components.
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