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#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
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"; }
43 std::string GetWorkflowGroup() const override { return "Game State"; }
44 std::string GetWorkflowDescription() const override {
45 return "Inspect progression, SRAM-derived flags, and live game state";
46 }
47 bool IsEnabled() const override {
49 auto* backend = GetProgressionBackend();
50 return project != nullptr && project->project_opened() &&
51 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(tr("SRAM (.srm)"));
110
112 options.filters = {
113 {"SRAM (.srm)", "srm"},
114 {"All Files", "*"},
115 };
116
117 if (ImGui::Button(tr("Import..."))) {
118 std::string file_path =
120 if (!file_path.empty()) {
121 auto state_or =
124 file_path)
126 if (state_or.ok()) {
127 state_ = *state_or;
128 game_state_slider_ = static_cast<int>(state_.game_state);
129 loaded_srm_path_ = file_path;
130 last_srm_error_.clear();
131 } else {
132 last_srm_error_ = std::string(state_or.status().message());
133 }
134 }
135 }
136
137 ImGui::SameLine();
138 if (ImGui::Button(tr("Clear"))) {
141 loaded_srm_path_.clear();
142 last_srm_error_.clear();
143 if (manifest_) {
144 if (auto* backend = GetProgressionBackend()) {
145 backend->ClearProgressionState(*manifest_);
146 } else {
148 }
149 }
150 }
151
152 if (!loaded_srm_path_.empty()) {
153 const std::filesystem::path p(loaded_srm_path_);
154 ImGui::TextDisabled(tr("Loaded: %s"), p.filename().string().c_str());
155 if (ImGui::IsItemHovered()) {
156 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
157 }
158 } else {
159 ImGui::TextDisabled(tr("Loaded: (none)"));
160 }
161
162 if (!last_srm_error_.empty()) {
163 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), tr("Error: %s"),
164 last_srm_error_.c_str());
165 }
166
168 }
169
171 const bool connected = live_client_ && live_client_->IsConnected();
172
173 ImGui::Spacing();
174 ImGui::Text(tr("Live SRAM (Mesen2)"));
175 ImGui::SameLine();
176 if (connected) {
177 ImGui::TextColored(ImVec4(0.25f, 0.85f, 0.35f, 1.0f), tr("Connected"));
178 } else {
179 ImGui::TextDisabled(tr("Disconnected"));
180 }
181
182 if (ImGui::SmallButton(tr("Sync from Mesen"))) {
183 live_refresh_pending_.store(false);
185 }
186 ImGui::SameLine();
187 ImGui::Checkbox(tr("Auto (event-driven)"), &live_sync_enabled_);
188 if (live_sync_enabled_) {
189 ImGui::SameLine();
190 ImGui::SetNextItemWidth(70.0f);
191 ImGui::SliderFloat("##LiveRefreshInterval",
192 &live_refresh_interval_seconds_, 0.05f, 0.5f, "%.2fs");
193 }
194
195 if (!connected) {
196 ImGui::TextDisabled(tr("Use a Mesen panel to connect (shared client)."));
197 }
198
199 if (!live_sync_error_.empty()) {
200 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), tr("Live sync: %s"),
201 live_sync_error_.c_str());
202 }
203 }
204
206 if (!manifest_ || !manifest_->loaded()) {
207 return;
208 }
209
210 const auto existing = manifest_->oracle_progression_state();
211 if (auto* backend = GetProgressionBackend()) {
212 const auto backend_existing = backend->GetProgressionState(*manifest_);
213 if (backend_existing.has_value() &&
214 StatesEqual(*backend_existing, state_)) {
215 return;
216 }
217 backend->SetProgressionState(*manifest_, state_);
218 return;
219 }
220
221 if (existing.has_value() && StatesEqual(*existing, state_)) {
222 return;
223 }
225 }
226
228 ImGui::Text(tr("Crystal Tracker"));
229 ImGui::Spacing();
230
231 float item_width = 44.0f;
232
233 for (int d = 1; d <= 7; ++d) {
235 bool complete = (state_.crystal_bitfield & mask) != 0;
236
237 ImVec4 color = complete ? ImVec4(0.2f, 0.8f, 0.3f, 1.0f) // green
238 : ImVec4(0.3f, 0.3f, 0.3f, 0.6f); // gray
239
240 {
241 gui::StyleColorGuard crystal_guard(
242 {{ImGuiCol_Button, color},
243 {ImGuiCol_ButtonHovered,
244 ImVec4(color.x + 0.1f, color.y + 0.1f, color.z + 0.1f, 1.0f)}});
245
246 char label[8];
247 snprintf(label, sizeof(label), "D%d", d);
248 if (ImGui::Button(label, ImVec2(item_width, 36.0f))) {
249 // Toggle crystal bit for testing
250 state_.crystal_bitfield ^= mask;
251 }
252 }
253
254 if (d < 7)
255 ImGui::SameLine();
256 }
257
258 ImGui::Text(tr("Crystals: %d / 7"), state_.GetCrystalCount());
259 }
260
262 ImGui::Text(tr("Game State"));
263 ImGui::Spacing();
264
265 // Phase labels
266 const char* phases[] = {"Start", "Loom Beach", "Kydrog Complete",
267 "Farore Rescued"};
268 int phase_count = 4;
269
270 float bar_width = ImGui::GetContentRegionAvail().x;
271 float segment = bar_width / static_cast<float>(phase_count);
272
273 ImVec2 bar_pos = ImGui::GetCursorScreenPos();
274 ImDrawList* draw_list = ImGui::GetWindowDrawList();
275
276 for (int i = 0; i < phase_count; ++i) {
277 ImVec2 seg_min(bar_pos.x + segment * i, bar_pos.y);
278 ImVec2 seg_max(bar_pos.x + segment * (i + 1), bar_pos.y + 24.0f);
279
280 ImU32 fill = (i <= state_.game_state) ? IM_COL32(60, 140, 200, 220)
281 : IM_COL32(50, 50, 50, 180);
282
283 draw_list->AddRectFilled(seg_min, seg_max, fill, 3.0f);
284 draw_list->AddRect(seg_min, seg_max, IM_COL32(80, 80, 80, 255), 3.0f);
285
286 // Label
287 ImVec2 text_pos(seg_min.x + 4, seg_min.y + 4);
288 draw_list->AddText(text_pos, IM_COL32(220, 220, 220, 255), phases[i]);
289 }
290
291 ImGui::Dummy(ImVec2(0, 30));
292 ImGui::Text(tr("Phase: %s"), state_.GetGameStateName().c_str());
293 }
294
296 ImGui::Text(tr("Dungeon Completion"));
297 ImGui::Spacing();
298
299 struct DungeonInfo {
300 const char* label;
301 int number; // 0 means special (FOS/SOP/SOW)
302 };
303
304 DungeonInfo dungeons[] = {
305 {"D1 Mushroom Grotto", 1}, {"D2 Tail Palace", 2},
306 {"D3 Kalyxo Castle", 3}, {"D4 Zora Temple", 4},
307 {"D5 Glacia Estate", 5}, {"D6 Goron Mines", 6},
308 {"D7 Dragon Ship", 7}, {"FOS Fortress", 0},
309 {"SOP Shrine of Power", 0}, {"SOW Shrine of Wisdom", 0},
310 };
311
312 ImGui::Columns(2, "dungeon_grid", false);
313 for (const auto& dungeon : dungeons) {
314 bool complete = false;
315 if (dungeon.number >= 1 && dungeon.number <= 7) {
316 complete = state_.IsDungeonComplete(dungeon.number);
317 }
318
319 ImVec4 color = 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"))
334 return;
335
336 // OOSPROG bits
337 ImGui::Text(tr("OOSPROG ($7EF3D6): 0x%02X"), state_.oosprog);
339
340 ImGui::Spacing();
341
342 // OOSPROG2 bits
343 ImGui::Text(tr("OOSPROG2 ($7EF3C6): 0x%02X"), state_.oosprog2);
345
346 ImGui::Spacing();
347
348 // Side quest
349 ImGui::Text(tr("SideQuest ($7EF3D7): 0x%02X"), state_.side_quest);
350
351 ImGui::TreePop();
352 }
353
354 static void DrawBitGrid(const char* id_prefix, uint8_t value,
355 const char* const* labels) {
356 for (int bit = 0; bit < 8; ++bit) {
357 bool set = (value & (1 << bit)) != 0;
358 ImVec4 color =
359 set ? ImVec4(0.3f, 0.7f, 0.3f, 1.0f) : ImVec4(0.2f, 0.2f, 0.2f, 0.6f);
360
361 char buf[64];
362 snprintf(buf, sizeof(buf), "%s##%s_%d", labels[bit], id_prefix, bit);
363
364 {
365 gui::StyleColorGuard bit_guard(ImGuiCol_Button, color);
366 ImGui::SmallButton(buf);
367 }
368 if (bit < 7)
369 ImGui::SameLine();
370 }
371 }
372
374 if (!ImGui::TreeNode("Manual Controls"))
375 return;
376
377 ImGui::SliderInt(tr("Game State"), &game_state_slider_, 0, 3);
379 state_.game_state = static_cast<uint8_t>(game_state_slider_);
380 }
381
382 int crystal_int = state_.crystal_bitfield;
383 if (ImGui::SliderInt(tr("Crystal Bits"), &crystal_int, 0, 127)) {
384 state_.crystal_bitfield = static_cast<uint8_t>(crystal_int);
385 }
386
387 if (ImGui::Button(tr("Clear All"))) {
390 }
391 ImGui::SameLine();
392 if (ImGui::Button(tr("Complete All"))) {
394 state_.game_state = 3;
396 }
397
398 ImGui::TreePop();
399 }
400
403 if (client == live_client_) {
404 return;
405 }
406
408 live_client_ = std::move(client);
410 live_sync_error_.clear();
411
412 if (live_client_ && live_client_->IsConnected()) {
413 live_refresh_pending_.store(true);
414 }
415 }
416
418 if (!live_sync_enabled_) {
419 return;
420 }
421 if (!live_client_ || !live_client_->IsConnected()) {
423 return;
424 }
425
426 if (live_listener_id_ == 0) {
427 live_listener_id_ = live_client_->AddEventListener(
428 [this](const emu::mesen::MesenEvent& event) {
429 if (event.type == "frame_complete" ||
430 event.type == "breakpoint_hit" || event.type == "all") {
431 live_refresh_pending_.store(true);
432 }
433 });
434 }
435
437 return;
438 }
439
440 const double now = ImGui::GetTime();
441 if ((now - last_subscribe_attempt_time_) < 1.0) {
442 return;
443 }
445
446 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
447 if (!status.ok()) {
448 live_sync_error_ = std::string(status.message());
449 return;
450 }
451
453 live_sync_error_.clear();
454 live_refresh_pending_.store(true);
455 }
456
458 if (!live_sync_enabled_) {
459 return;
460 }
461 if (!live_refresh_pending_.load()) {
462 return;
463 }
464 const double now = ImGui::GetTime();
466 return;
467 }
470 }
471 live_refresh_pending_.store(false);
472 }
473
475 if (!live_client_ || !live_client_->IsConnected()) {
476 live_sync_error_ = "Mesen client is not connected";
477 return false;
478 }
479
480 if (auto* backend = GetProgressionBackend()) {
481 auto state_or = backend->ReadProgressionStateFromLiveSram(*live_client_);
482 if (!state_or.ok()) {
483 live_sync_error_ = std::string(state_or.status().message());
484 return false;
485 }
486 state_ = *state_or;
487 game_state_slider_ = static_cast<int>(state_.game_state);
488 loaded_srm_path_ = "Mesen2 Live";
489 last_srm_error_.clear();
490 live_sync_error_.clear();
491 return true;
492 }
493
494 constexpr uint32_t kBaseAddress = 0x7EF000;
495 constexpr uint16_t kStartOffset =
497 constexpr uint16_t kEndOffset =
499 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
500 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
501
502 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
503 if (!bytes_or.ok()) {
504 live_sync_error_ = std::string(bytes_or.status().message());
505 return false;
506 }
507 if (bytes_or->size() < kReadLength) {
508 live_sync_error_ = "SRAM read returned truncated data";
509 return false;
510 }
511
512 const auto read_byte = [&](uint16_t offset) -> uint8_t {
513 return (*bytes_or)[offset - kStartOffset];
514 };
515
525 game_state_slider_ = static_cast<int>(state_.game_state);
526
527 loaded_srm_path_ = "Mesen2 Live";
528 last_srm_error_.clear();
529 live_sync_error_.clear();
530 return true;
531 }
532
534 if (live_client_ && live_listener_id_ != 0) {
535 live_client_->RemoveEventListener(live_listener_id_);
536 }
539 }
540
544
548
551
553 std::string loaded_srm_path_;
554 std::string last_srm_error_;
555
556 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
560 std::atomic<bool> live_refresh_pending_{false};
564 std::string live_sync_error_;
565
566 // Bit labels for flag grids
567 static constexpr const char* oosprog_labels_[8] = {
568 "Bit0", "HallOfSecrets", "PendantQuest", "Bit3",
569 "ElderMet", "Bit5", "Bit6", "FortressComplete",
570 };
571
572 static constexpr const char* oosprog2_labels_[8] = {
573 "Bit0", "Bit1", "KydrogEncounter",
574 "Bit3", "DekuSoulFreed", "BookOfSecrets",
575 "Bit6", "Bit7",
576 };
577};
578
579} // namespace yaze::editor
580
581#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