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
14#include "app/gui/core/icons.h"
16#include "core/hack_manifest.h"
19#include "core/project.h"
20#include "imgui/imgui.h"
21#include "util/file_util.h"
22
23namespace yaze::editor {
24
34 public:
36
37 std::string GetId() const override { return "oracle.progression_dashboard"; }
38 std::string GetDisplayName() const override {
39 return "Progression Dashboard";
40 }
41 std::string GetIcon() const override { return ICON_MD_DASHBOARD; }
42 std::string GetEditorCategory() const override { return "Oracle"; }
45 }
46 float GetPreferredWidth() const override { return 400.0f; }
47
48 void Draw(bool* p_open) override {
49 (void)p_open;
50
54
55 // Lazily resolve the manifest from the project context.
56 if (!manifest_) {
58 if (project && project->hack_manifest.loaded()) {
59 manifest_ = &project->hack_manifest;
60 }
61 }
62
64 ImGui::Separator();
65
67 ImGui::Separator();
69 ImGui::Separator();
71 ImGui::Separator();
73 ImGui::Separator();
75
77 }
78
79 private:
82 return a.crystal_bitfield == b.crystal_bitfield &&
83 a.game_state == b.game_state && a.oosprog == b.oosprog &&
84 a.oosprog2 == b.oosprog2 && a.side_quest == b.side_quest &&
85 a.pendants == b.pendants;
86 }
87
89 ImGui::Text("SRAM (.srm)");
90
92 options.filters = {
93 {"SRAM (.srm)", "srm"},
94 {"All Files", "*"},
95 };
96
97 if (ImGui::Button("Import...")) {
98 std::string file_path =
100 if (!file_path.empty()) {
101 auto state_or = core::LoadOracleProgressionFromSrmFile(file_path);
102 if (state_or.ok()) {
103 state_ = *state_or;
104 game_state_slider_ = static_cast<int>(state_.game_state);
105 loaded_srm_path_ = file_path;
106 last_srm_error_.clear();
107 } else {
108 last_srm_error_ = std::string(state_or.status().message());
109 }
110 }
111 }
112
113 ImGui::SameLine();
114 if (ImGui::Button("Clear")) {
117 loaded_srm_path_.clear();
118 last_srm_error_.clear();
119 if (manifest_) {
121 }
122 }
123
124 if (!loaded_srm_path_.empty()) {
125 const std::filesystem::path p(loaded_srm_path_);
126 ImGui::TextDisabled("Loaded: %s", p.filename().string().c_str());
127 if (ImGui::IsItemHovered()) {
128 ImGui::SetTooltip("%s", loaded_srm_path_.c_str());
129 }
130 } else {
131 ImGui::TextDisabled("Loaded: (none)");
132 }
133
134 if (!last_srm_error_.empty()) {
135 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Error: %s",
136 last_srm_error_.c_str());
137 }
138
140 }
141
143 const bool connected = live_client_ && live_client_->IsConnected();
144
145 ImGui::Spacing();
146 ImGui::Text("Live SRAM (Mesen2)");
147 ImGui::SameLine();
148 if (connected) {
149 ImGui::TextColored(ImVec4(0.25f, 0.85f, 0.35f, 1.0f), "Connected");
150 } else {
151 ImGui::TextDisabled("Disconnected");
152 }
153
154 if (ImGui::SmallButton("Sync from Mesen")) {
155 live_refresh_pending_.store(false);
157 }
158 ImGui::SameLine();
159 ImGui::Checkbox("Auto (event-driven)", &live_sync_enabled_);
160 if (live_sync_enabled_) {
161 ImGui::SameLine();
162 ImGui::SetNextItemWidth(70.0f);
163 ImGui::SliderFloat("##LiveRefreshInterval", &live_refresh_interval_seconds_,
164 0.05f, 0.5f, "%.2fs");
165 }
166
167 if (!connected) {
168 ImGui::TextDisabled("Use a Mesen panel to connect (shared client).");
169 }
170
171 if (!live_sync_error_.empty()) {
172 ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.35f, 1.0f), "Live sync: %s",
173 live_sync_error_.c_str());
174 }
175 }
176
178 if (!manifest_ || !manifest_->loaded()) {
179 return;
180 }
181
182 const auto existing = manifest_->oracle_progression_state();
183 if (existing.has_value() && StatesEqual(*existing, state_)) {
184 return;
185 }
187 }
188
190 ImGui::Text("Crystal Tracker");
191 ImGui::Spacing();
192
193 float item_width = 44.0f;
194
195 for (int d = 1; d <= 7; ++d) {
197 bool complete = (state_.crystal_bitfield & mask) != 0;
198
199 ImVec4 color =
200 complete ? ImVec4(0.2f, 0.8f, 0.3f, 1.0f) // green
201 : ImVec4(0.3f, 0.3f, 0.3f, 0.6f); // gray
202
203 {
204 gui::StyleColorGuard crystal_guard(
205 {{ImGuiCol_Button, color},
206 {ImGuiCol_ButtonHovered,
207 ImVec4(color.x + 0.1f, color.y + 0.1f, color.z + 0.1f, 1.0f)}});
208
209 char label[8];
210 snprintf(label, sizeof(label), "D%d", d);
211 if (ImGui::Button(label, ImVec2(item_width, 36.0f))) {
212 // Toggle crystal bit for testing
213 state_.crystal_bitfield ^= mask;
214 }
215 }
216
217 if (d < 7) ImGui::SameLine();
218 }
219
220 ImGui::Text("Crystals: %d / 7", state_.GetCrystalCount());
221 }
222
224 ImGui::Text("Game State");
225 ImGui::Spacing();
226
227 // Phase labels
228 const char* phases[] = {"Start", "Loom Beach", "Kydrog Complete",
229 "Farore Rescued"};
230 int phase_count = 4;
231
232 float bar_width = ImGui::GetContentRegionAvail().x;
233 float segment = bar_width / static_cast<float>(phase_count);
234
235 ImVec2 bar_pos = ImGui::GetCursorScreenPos();
236 ImDrawList* draw_list = ImGui::GetWindowDrawList();
237
238 for (int i = 0; i < phase_count; ++i) {
239 ImVec2 seg_min(bar_pos.x + segment * i, bar_pos.y);
240 ImVec2 seg_max(bar_pos.x + segment * (i + 1), bar_pos.y + 24.0f);
241
242 ImU32 fill = (i <= state_.game_state)
243 ? IM_COL32(60, 140, 200, 220)
244 : IM_COL32(50, 50, 50, 180);
245
246 draw_list->AddRectFilled(seg_min, seg_max, fill, 3.0f);
247 draw_list->AddRect(seg_min, seg_max, IM_COL32(80, 80, 80, 255), 3.0f);
248
249 // Label
250 ImVec2 text_pos(seg_min.x + 4, seg_min.y + 4);
251 draw_list->AddText(text_pos, IM_COL32(220, 220, 220, 255), phases[i]);
252 }
253
254 ImGui::Dummy(ImVec2(0, 30));
255 ImGui::Text("Phase: %s", state_.GetGameStateName().c_str());
256 }
257
259 ImGui::Text("Dungeon Completion");
260 ImGui::Spacing();
261
262 struct DungeonInfo {
263 const char* label;
264 int number; // 0 means special (FOS/SOP/SOW)
265 };
266
267 DungeonInfo dungeons[] = {
268 {"D1 Mushroom Grotto", 1}, {"D2 Tail Palace", 2},
269 {"D3 Kalyxo Castle", 3}, {"D4 Zora Temple", 4},
270 {"D5 Glacia Estate", 5}, {"D6 Goron Mines", 6},
271 {"D7 Dragon Ship", 7}, {"FOS Fortress", 0},
272 {"SOP Shrine of Power", 0}, {"SOW Shrine of Wisdom", 0},
273 };
274
275 ImGui::Columns(2, "dungeon_grid", false);
276 for (const auto& dungeon : dungeons) {
277 bool complete = false;
278 if (dungeon.number >= 1 && dungeon.number <= 7) {
279 complete = state_.IsDungeonComplete(dungeon.number);
280 }
281
282 ImVec4 color =
283 complete ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f) // green
284 : ImVec4(0.25f, 0.25f, 0.25f, 1.0f); // dark
285
286 {
287 gui::StyleColorGuard header_guard(ImGuiCol_Header, color);
288 ImGui::Selectable(dungeon.label, complete,
289 ImGuiSelectableFlags_Disabled);
290 }
291 ImGui::NextColumn();
292 }
293 ImGui::Columns(1);
294 }
295
297 if (!ImGui::TreeNode("Story Flags")) return;
298
299 // OOSPROG bits
300 ImGui::Text("OOSPROG ($7EF3D6): 0x%02X", state_.oosprog);
302
303 ImGui::Spacing();
304
305 // OOSPROG2 bits
306 ImGui::Text("OOSPROG2 ($7EF3C6): 0x%02X", state_.oosprog2);
308
309 ImGui::Spacing();
310
311 // Side quest
312 ImGui::Text("SideQuest ($7EF3D7): 0x%02X", state_.side_quest);
313
314 ImGui::TreePop();
315 }
316
317 static void DrawBitGrid(const char* id_prefix, uint8_t value,
318 const char* const* labels) {
319 for (int bit = 0; bit < 8; ++bit) {
320 bool set = (value & (1 << bit)) != 0;
321 ImVec4 color = set ? ImVec4(0.3f, 0.7f, 0.3f, 1.0f)
322 : ImVec4(0.2f, 0.2f, 0.2f, 0.6f);
323
324 char buf[64];
325 snprintf(buf, sizeof(buf), "%s##%s_%d", labels[bit], id_prefix, bit);
326
327 {
328 gui::StyleColorGuard bit_guard(ImGuiCol_Button, color);
329 ImGui::SmallButton(buf);
330 }
331 if (bit < 7) ImGui::SameLine();
332 }
333 }
334
336 if (!ImGui::TreeNode("Manual Controls")) return;
337
338 ImGui::SliderInt("Game State", &game_state_slider_, 0, 3);
340 state_.game_state = static_cast<uint8_t>(game_state_slider_);
341 }
342
343 int crystal_int = state_.crystal_bitfield;
344 if (ImGui::SliderInt("Crystal Bits", &crystal_int, 0, 127)) {
345 state_.crystal_bitfield = static_cast<uint8_t>(crystal_int);
346 }
347
348 if (ImGui::Button("Clear All")) {
351 }
352 ImGui::SameLine();
353 if (ImGui::Button("Complete All")) {
355 state_.game_state = 3;
357 }
358
359 ImGui::TreePop();
360 }
361
364 if (client == live_client_) {
365 return;
366 }
367
369 live_client_ = std::move(client);
371 live_sync_error_.clear();
372
373 if (live_client_ && live_client_->IsConnected()) {
374 live_refresh_pending_.store(true);
375 }
376 }
377
379 if (!live_sync_enabled_) {
380 return;
381 }
382 if (!live_client_ || !live_client_->IsConnected()) {
384 return;
385 }
386
387 if (live_listener_id_ == 0) {
389 live_client_->AddEventListener([this](const emu::mesen::MesenEvent& event) {
390 if (event.type == "frame_complete" ||
391 event.type == "breakpoint_hit" || event.type == "all") {
392 live_refresh_pending_.store(true);
393 }
394 });
395 }
396
398 return;
399 }
400
401 const double now = ImGui::GetTime();
402 if ((now - last_subscribe_attempt_time_) < 1.0) {
403 return;
404 }
406
407 auto status = live_client_->Subscribe({"frame_complete", "breakpoint_hit"});
408 if (!status.ok()) {
409 live_sync_error_ = std::string(status.message());
410 return;
411 }
412
414 live_sync_error_.clear();
415 live_refresh_pending_.store(true);
416 }
417
419 if (!live_sync_enabled_) {
420 return;
421 }
422 if (!live_refresh_pending_.load()) {
423 return;
424 }
425 const double now = ImGui::GetTime();
427 return;
428 }
431 }
432 live_refresh_pending_.store(false);
433 }
434
436 if (!live_client_ || !live_client_->IsConnected()) {
437 live_sync_error_ = "Mesen client is not connected";
438 return false;
439 }
440
441 constexpr uint32_t kBaseAddress = 0x7EF000;
442 constexpr uint16_t kStartOffset = core::OracleProgressionState::kPendantOffset;
443 constexpr uint16_t kEndOffset = core::OracleProgressionState::kSideQuestOffset;
444 constexpr size_t kReadLength = kEndOffset - kStartOffset + 1;
445 constexpr uint32_t kReadAddress = kBaseAddress + kStartOffset;
446
447 auto bytes_or = live_client_->ReadBlock(kReadAddress, kReadLength);
448 if (!bytes_or.ok()) {
449 live_sync_error_ = std::string(bytes_or.status().message());
450 return false;
451 }
452 if (bytes_or->size() < kReadLength) {
453 live_sync_error_ = "SRAM read returned truncated data";
454 return false;
455 }
456
457 const auto read_byte = [&](uint16_t offset) -> uint8_t {
458 return (*bytes_or)[offset - kStartOffset];
459 };
460
468 game_state_slider_ = static_cast<int>(state_.game_state);
469
470 loaded_srm_path_ = "Mesen2 Live";
471 last_srm_error_.clear();
472 live_sync_error_.clear();
473 return true;
474 }
475
477 if (live_client_ && live_listener_id_ != 0) {
478 live_client_->RemoveEventListener(live_listener_id_);
479 }
482 }
483
486
488 std::string loaded_srm_path_;
489 std::string last_srm_error_;
490
491 std::shared_ptr<emu::mesen::MesenSocketClient> live_client_;
495 std::atomic<bool> live_refresh_pending_{false};
499 std::string live_sync_error_;
500
501 // Bit labels for flag grids
502 static constexpr const char* oosprog_labels_[8] = {
503 "Bit0", "HallOfSecrets", "PendantQuest", "Bit3",
504 "ElderMet", "Bit5", "Bit6", "FortressComplete",
505 };
506
507 static constexpr const char* oosprog2_labels_[8] = {
508 "Bit0", "Bit1", "KydrogEncounter", "Bit3",
509 "DekuSoulFreed", "BookOfSecrets", "Bit6", "Bit7",
510 };
511};
512
513} // namespace yaze::editor
514
515#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)
Base interface for all logical panel components.
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.
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.
void Draw(bool *p_open) override
Draw the panel content.
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
PanelCategory GetPanelCategory() const override
Get the lifecycle category for this panel.
std::string GetId() const override
Unique identifier for this panel.
std::shared_ptr< emu::mesen::MesenSocketClient > live_client_
static constexpr const char * oosprog2_labels_[8]
std::string GetEditorCategory() const override
Editor category this panel belongs to.
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.
Editors are the view controllers for the application.
PanelCategory
Defines lifecycle behavior for editor panels.
@ 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