yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sram_viewer_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7
8#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
12#include "core/project.h"
13#include "imgui/imgui.h"
14
15namespace yaze {
16namespace editor {
17
18namespace {
19
20// Address range boundaries for grouping
21constexpr uint32_t kStoryRangeStart = 0x7EF3C5;
22constexpr uint32_t kStoryRangeEnd = 0x7EF3D8;
23constexpr uint32_t kDungeonCrystals = 0x7EF37A;
24constexpr uint32_t kDungeonPendants = 0x7EF374;
25constexpr uint32_t kItemsRangeStart = 0x7EF340;
26constexpr uint32_t kItemsRangeEnd = 0x7EF380;
27
28// Crystal bitfield labels (bit index -> dungeon name)
29struct CrystalBit {
30 uint8_t mask;
31 const char* label;
32};
33constexpr CrystalBit kCrystalBits[] = {
34 {0x01, "D1 Mushroom Grotto"},
35 {0x02, "D6 Goron Mines"},
36 {0x04, "D5 Glacia Estate"},
37 {0x08, "D7 Dragon Ship"},
38 {0x10, "D2 Tail Palace"},
39 {0x20, "D4 Zora Temple"},
40 {0x40, "D3 Kalyxo Castle"},
41};
42
43// GameState enum labels
44const char* kGameStateLabels[] = {
45 "0: Start",
46 "1: LoomBeach",
47 "2: KydrogComplete",
48 "3: FaroreRescued",
49};
50constexpr int kGameStateLabelCount = 4;
51
52// How long the yellow highlight lasts after a value changes (seconds)
53constexpr float kChangeHighlightDuration = 2.0f;
54
55bool IsInStoryRange(uint32_t addr) {
56 return addr >= kStoryRangeStart && addr < kStoryRangeEnd;
57}
58
59bool IsDungeonAddress(uint32_t addr) {
60 return addr == kDungeonCrystals || addr == kDungeonPendants;
61}
62
63bool IsInItemsRange(uint32_t addr) {
64 return addr >= kItemsRangeStart && addr < kItemsRangeEnd;
65}
66
67} // namespace
68
78
80
82 return client_ && client_->IsConnected();
83}
84
86 if (!client_) {
88 }
89 auto status = client_->Connect();
90 if (!status.ok()) {
91 connection_error_ = std::string(status.message());
92 } else {
93 connection_error_.clear();
96 }
97}
98
99void SramViewerPanel::ConnectToPath(const std::string& socket_path) {
100 if (!client_) {
102 }
103 auto status = client_->Connect(socket_path);
104 if (!status.ok()) {
105 connection_error_ = std::string(status.message());
106 } else {
107 connection_error_.clear();
110 }
111}
112
114 if (client_) {
115 client_->Disconnect();
116 }
117}
118
121 if (!socket_paths_.empty()) {
122 if (selected_socket_index_ < 0 ||
123 selected_socket_index_ >= static_cast<int>(socket_paths_.size())) {
125 }
126 if (socket_path_buffer_[0] == '\0') {
127 std::snprintf(socket_path_buffer_, sizeof(socket_path_buffer_), "%s",
129 }
130 } else {
132 }
133}
134
136 variables_.clear();
137 variables_loaded_ = false;
138
139 if (!project_) return;
140 if (!project_->hack_manifest.loaded()) return;
141
143 variables_loaded_ = true;
144
145 // Sort by address for consistent display
146 std::sort(variables_.begin(), variables_.end(),
147 [](const core::SramVariable& a, const core::SramVariable& b) {
148 return a.address < b.address;
149 });
150}
151
153 if (!IsConnected()) return;
154 if (variables_.empty()) return;
155
156 // Save previous values for change detection
158
159 // Read each variable individually (they may be scattered across WRAM)
160 float current_time = static_cast<float>(ImGui::GetTime());
161 for (const auto& var : variables_) {
162 auto result = client_->ReadByte(var.address);
163 if (result.ok()) {
164 uint8_t new_value = *result;
165
166 // Detect changes
167 auto prev_it = previous_values_.find(var.address);
168 if (prev_it != previous_values_.end() && prev_it->second != new_value) {
169 change_timestamps_[var.address] = current_time;
170 }
171
172 current_values_[var.address] = new_value;
173 }
174 }
175}
176
177void SramViewerPanel::PokeValue(uint32_t address, uint8_t value) {
178 if (!IsConnected()) return;
179
180 auto status = client_->WriteByte(address, value);
181 if (!status.ok()) {
183 absl::StrFormat("Write failed: %s", status.message());
184 } else {
186 absl::StrFormat("Wrote $%02X to $%06X", value, address);
187 // Update cache immediately
188 current_values_[address] = value;
189 }
190}
191
193 ImGui::PushID("SramViewerPanel");
194
195 // Load variables from manifest on first draw (or when project changes)
196 if (!variables_loaded_ && project_) {
198 }
199
200 // Auto-refresh logic
201 if (IsConnected() && auto_refresh_) {
202 time_since_refresh_ += ImGui::GetIO().DeltaTime;
205 time_since_refresh_ = 0.0f;
206 }
207 }
208
210 if (ImGui::BeginChild("SramViewer_Panel", ImVec2(0, 0), true)) {
211 if (ImGui::IsWindowAppearing()) {
213 if (project_) {
215 }
216 }
218
219 if (IsConnected()) {
220 ImGui::Spacing();
221
222 if (variables_.empty()) {
223 ImGui::TextDisabled(
224 "No SRAM variables loaded. Ensure hack_manifest.json is present "
225 "in the project.");
226 } else {
227 // Filter
228 ImGui::TextDisabled("Filter");
229 ImGui::SameLine();
230 ImGui::SetNextItemWidth(-1);
231 ImGui::InputTextWithHint("##sram_filter", "Search by name or address",
232 filter_text_, sizeof(filter_text_));
233 ImGui::Spacing();
234
236 }
237 } else if (!variables_.empty()) {
238 // Show variables even when disconnected (no values)
239 ImGui::Spacing();
240 ImGui::TextDisabled("Connect to Mesen2 to see live values.");
241 }
242 }
243 ImGui::EndChild();
245
246 ImGui::PopID();
247}
248
250 const auto& theme = AgentUI::GetTheme();
251
252 ImGui::TextColored(theme.accent_color, "%s SRAM Viewer",
254
255 // Connection status indicator
256 ImGui::SameLine(ImGui::GetWindowWidth() - 100);
257 if (IsConnected()) {
258 float pulse = 0.7f + 0.3f * std::sin(ImGui::GetTime() * 2.0f);
259 ImVec4 connected_color = ImVec4(0.1f, pulse, 0.3f, 1.0f);
260 ImGui::TextColored(connected_color, "%s Connected", ICON_MD_CHECK_CIRCLE);
261 } else {
262 ImGui::TextColored(theme.status_error, "%s Disconnected", ICON_MD_ERROR);
263 }
264
265 ImGui::Separator();
266
267 if (!IsConnected()) {
268 ImGui::TextDisabled("Socket");
269 const char* preview =
271 selected_socket_index_ < static_cast<int>(socket_paths_.size()))
273 : "No sockets found";
274 ImGui::SetNextItemWidth(-40);
275 if (ImGui::BeginCombo("##sram_socket_combo", preview)) {
276 for (int i = 0; i < static_cast<int>(socket_paths_.size()); ++i) {
277 bool selected = (i == selected_socket_index_);
278 if (ImGui::Selectable(socket_paths_[i].c_str(), selected)) {
280 std::snprintf(socket_path_buffer_, sizeof(socket_path_buffer_), "%s",
281 socket_paths_[i].c_str());
282 }
283 if (selected) {
284 ImGui::SetItemDefaultFocus();
285 }
286 }
287 ImGui::EndCombo();
288 }
289 ImGui::SameLine();
290 if (ImGui::SmallButton(ICON_MD_REFRESH "##sram_refresh")) {
292 }
293
294 ImGui::TextDisabled("Path");
295 ImGui::SetNextItemWidth(-1);
296 ImGui::InputTextWithHint("##sram_socket_path", "/tmp/mesen2-12345.sock",
298
299 if (ImGui::Button(ICON_MD_LINK " Connect")) {
300 std::string path = socket_path_buffer_;
301 if (path.empty() && selected_socket_index_ >= 0 &&
302 selected_socket_index_ < static_cast<int>(socket_paths_.size())) {
304 }
305 if (path.empty()) {
306 Connect();
307 } else {
308 ConnectToPath(path);
309 }
310 }
311 ImGui::SameLine();
312 if (ImGui::SmallButton(ICON_MD_AUTO_MODE " Auto")) {
313 Connect();
314 }
315 if (!connection_error_.empty()) {
316 ImGui::Spacing();
317 ImGui::TextColored(theme.status_error, "%s", connection_error_.c_str());
318 }
319 } else {
320 if (ImGui::Button(ICON_MD_LINK_OFF " Disconnect")) {
321 Disconnect();
322 }
323 ImGui::SameLine();
324 ImGui::Checkbox("Auto-refresh", &auto_refresh_);
325 if (auto_refresh_) {
326 ImGui::SameLine();
327 ImGui::SetNextItemWidth(60);
328 ImGui::SliderFloat("##SramRefreshRate", &refresh_interval_, 0.05f, 1.0f,
329 "%.2fs");
330 } else {
331 ImGui::SameLine();
332 if (ImGui::SmallButton(ICON_MD_REFRESH " Refresh")) {
334 }
335 }
336 }
337
338 if (!status_message_.empty()) {
339 ImGui::Spacing();
340 ImGui::TextColored(theme.text_secondary_color, "%s",
341 status_message_.c_str());
342 }
343}
344
346 // Partition variables into groups
347 std::vector<const core::SramVariable*> story_vars;
348 std::vector<const core::SramVariable*> dungeon_vars;
349 std::vector<const core::SramVariable*> item_vars;
350 std::vector<const core::SramVariable*> other_vars;
351
352 std::string filter_lower;
353 if (filter_text_[0] != '\0') {
354 filter_lower = filter_text_;
355 std::transform(filter_lower.begin(), filter_lower.end(),
356 filter_lower.begin(), ::tolower);
357 }
358
359 for (const auto& var : variables_) {
360 // Apply filter
361 if (!filter_lower.empty()) {
362 std::string name_lower = var.name;
363 std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
364 ::tolower);
365 std::string purpose_lower = var.purpose;
366 std::transform(purpose_lower.begin(), purpose_lower.end(),
367 purpose_lower.begin(), ::tolower);
368 std::string addr_str = absl::StrFormat("$%06X", var.address);
369 std::string addr_lower = addr_str;
370 std::transform(addr_lower.begin(), addr_lower.end(), addr_lower.begin(),
371 ::tolower);
372
373 if (name_lower.find(filter_lower) == std::string::npos &&
374 purpose_lower.find(filter_lower) == std::string::npos &&
375 addr_lower.find(filter_lower) == std::string::npos) {
376 continue;
377 }
378 }
379
380 if (IsInStoryRange(var.address)) {
381 story_vars.push_back(&var);
382 } else if (IsDungeonAddress(var.address)) {
383 dungeon_vars.push_back(&var);
384 } else if (IsInItemsRange(var.address)) {
385 item_vars.push_back(&var);
386 } else {
387 other_vars.push_back(&var);
388 }
389 }
390
391 // Story section
392 if (!story_vars.empty()) {
393 std::string story_label =
394 absl::StrFormat("%s Story (%zu)", ICON_MD_AUTO_STORIES,
395 story_vars.size());
396 if (ImGui::CollapsingHeader(
397 story_label.c_str(),
398 story_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
399 story_expanded_ = true;
400 for (const auto* var : story_vars) {
401 DrawVariableRow(*var);
402 }
403 } else {
404 story_expanded_ = false;
405 }
406 }
407
408 // Dungeon section
409 if (!dungeon_vars.empty()) {
410 std::string dungeon_label =
411 absl::StrFormat("%s Dungeon (%zu)", ICON_MD_CASTLE,
412 dungeon_vars.size());
413 if (ImGui::CollapsingHeader(
414 dungeon_label.c_str(),
415 dungeon_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
416 dungeon_expanded_ = true;
417 for (const auto* var : dungeon_vars) {
418 DrawVariableRow(*var);
419 }
420 } else {
421 dungeon_expanded_ = false;
422 }
423 }
424
425 // Items section
426 if (!item_vars.empty()) {
427 std::string items_label =
428 absl::StrFormat("%s Items (%zu)", ICON_MD_INVENTORY_2,
429 item_vars.size());
430 if (ImGui::CollapsingHeader(
431 items_label.c_str(),
432 items_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
433 items_expanded_ = true;
434 for (const auto* var : item_vars) {
435 DrawVariableRow(*var);
436 }
437 } else {
438 items_expanded_ = false;
439 }
440 }
441
442 // Other/uncategorized
443 if (!other_vars.empty()) {
444 std::string other_label =
445 absl::StrFormat("%s Other (%zu)", ICON_MD_MORE_HORIZ,
446 other_vars.size());
447 if (ImGui::CollapsingHeader(other_label.c_str())) {
448 for (const auto* var : other_vars) {
449 DrawVariableRow(*var);
450 }
451 }
452 }
453}
454
456 const auto& theme = AgentUI::GetTheme();
457 float current_time = static_cast<float>(ImGui::GetTime());
458
459 ImGui::PushID(static_cast<int>(var.address));
460
461 // Check if this value recently changed
462 bool recently_changed = false;
463 auto ts_it = change_timestamps_.find(var.address);
464 if (ts_it != change_timestamps_.end()) {
465 float elapsed = current_time - ts_it->second;
466 if (elapsed < kChangeHighlightDuration) {
467 recently_changed = true;
468 // Flash yellow background that fades out
469 float alpha = 1.0f - (elapsed / kChangeHighlightDuration);
470 ImVec4 highlight_color = ImVec4(0.9f, 0.8f, 0.1f, alpha * 0.3f);
471 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
472 ImVec2 row_size = ImVec2(ImGui::GetContentRegionAvail().x,
473 ImGui::GetTextLineHeightWithSpacing());
474 ImGui::GetWindowDrawList()->AddRectFilled(
475 cursor_pos, ImVec2(cursor_pos.x + row_size.x,
476 cursor_pos.y + row_size.y),
477 ImGui::ColorConvertFloat4ToU32(highlight_color));
478 }
479 }
480
481 // Address column
482 ImGui::TextColored(theme.text_secondary_color, "$%06X", var.address);
483
484 // Name column
485 ImGui::SameLine(80);
486 if (recently_changed) {
487 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.2f, 1.0f), "%s",
488 var.name.c_str());
489 } else {
490 ImGui::Text("%s", var.name.c_str());
491 }
492
493 // Purpose (tooltip)
494 if (!var.purpose.empty() && ImGui::IsItemHovered()) {
495 ImGui::SetTooltip("%s", var.purpose.c_str());
496 }
497
498 // Value columns (only when connected and we have data)
499 auto val_it = current_values_.find(var.address);
500 if (val_it != current_values_.end()) {
501 uint8_t value = val_it->second;
502
503 // Decimal value
504 ImGui::SameLine(240);
505 ImGui::Text("%d", value);
506
507 // Hex value
508 ImGui::SameLine(290);
509 ImGui::TextColored(theme.text_secondary_color, "$%02X", value);
510
511 // Edit button
512 ImGui::SameLine(340);
513 std::string edit_label = absl::StrFormat(
514 "%s##edit_%06X", ICON_MD_EDIT, var.address);
515 if (ImGui::SmallButton(edit_label.c_str())) {
516 editing_active_ = true;
518 editing_value_ = value;
519 ImGui::OpenPopup("SramEditPopup");
520 }
521 }
522
523 // Special expansions for key addresses
524 if (val_it != current_values_.end()) {
525 if (var.address == kDungeonCrystals) {
526 DrawCrystalBitfield(val_it->second, var.address);
527 } else if (var.address == kStoryRangeStart) {
528 // GameState is the first story address ($7EF3C5)
529 DrawGameStateDropdown(val_it->second, var.address);
530 }
531 }
532
533 ImGui::PopID();
534}
535
536void SramViewerPanel::DrawCrystalBitfield(uint8_t value, uint32_t address) {
537 ImGui::Indent(20);
538 bool any_changed = false;
539 uint8_t new_value = value;
540
541 for (const auto& bit : kCrystalBits) {
542 bool set = (value & bit.mask) != 0;
543 std::string cb_label = absl::StrFormat("%s##crystal_%02X", bit.label,
544 bit.mask);
545 if (ImGui::Checkbox(cb_label.c_str(), &set)) {
546 if (set) {
547 new_value |= bit.mask;
548 } else {
549 new_value &= ~bit.mask;
550 }
551 any_changed = true;
552 }
553 }
554
555 if (any_changed) {
556 PokeValue(address, new_value);
557 }
558
559 ImGui::Unindent(20);
560}
561
562void SramViewerPanel::DrawGameStateDropdown(uint8_t value, uint32_t address) {
563 ImGui::Indent(20);
564
565 int current_index = value;
566 if (current_index >= kGameStateLabelCount) {
567 current_index = -1; // Unknown state
568 }
569
570 const char* preview = (current_index >= 0 && current_index < kGameStateLabelCount)
571 ? kGameStateLabels[current_index]
572 : "Unknown";
573
574 ImGui::SetNextItemWidth(200);
575 if (ImGui::BeginCombo("##gamestate_combo", preview)) {
576 for (int i = 0; i < kGameStateLabelCount; ++i) {
577 bool selected = (i == current_index);
578 if (ImGui::Selectable(kGameStateLabels[i], selected)) {
579 PokeValue(address, static_cast<uint8_t>(i));
580 }
581 if (selected) {
582 ImGui::SetItemDefaultFocus();
583 }
584 }
585 ImGui::EndCombo();
586 }
587
588 ImGui::Unindent(20);
589}
590
591} // namespace editor
592} // namespace yaze
const std::vector< SramVariable > & sram_variables() const
bool loaded() const
Check if the manifest has been loaded.
void DrawCrystalBitfield(uint8_t value, uint32_t address)
void DrawGameStateDropdown(uint8_t value, uint32_t address)
std::vector< std::string > socket_paths_
project::YazeProject * project_
void PokeValue(uint32_t address, uint8_t value)
std::vector< core::SramVariable > variables_
std::unordered_map< uint32_t, uint8_t > previous_values_
std::unordered_map< uint32_t, uint8_t > current_values_
std::unordered_map< uint32_t, float > change_timestamps_
void ConnectToPath(const std::string &socket_path)
std::shared_ptr< emu::mesen::MesenSocketClient > client_
void DrawVariableRow(const core::SramVariable &var)
static void SetClient(std::shared_ptr< MesenSocketClient > client)
static std::shared_ptr< MesenSocketClient > GetOrCreate()
static std::vector< std::string > ListAvailableSockets()
List available Mesen2 sockets on the system.
#define ICON_MD_LINK
Definition icons.h:1090
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_LINK_OFF
Definition icons.h:1091
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_AUTO_MODE
Definition icons.h:222
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_AUTO_STORIES
Definition icons.h:223
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
const AgentUITheme & GetTheme()
A custom SRAM variable definition.
core::HackManifest hack_manifest
Definition project.h:160