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"}, {0x02, "D6 Goron Mines"},
35 {0x04, "D5 Glacia Estate"}, {0x08, "D7 Dragon Ship"},
36 {0x10, "D2 Tail Palace"}, {0x20, "D4 Zora Temple"},
37 {0x40, "D3 Kalyxo Castle"},
38};
39
40// GameState enum labels
41const char* kGameStateLabels[] = {
42 "0: Start",
43 "1: LoomBeach",
44 "2: KydrogComplete",
45 "3: FaroreRescued",
46};
47constexpr int kGameStateLabelCount = 4;
48
49// How long the yellow highlight lasts after a value changes (seconds)
50constexpr float kChangeHighlightDuration = 2.0f;
51
52bool IsInStoryRange(uint32_t addr) {
53 return addr >= kStoryRangeStart && addr < kStoryRangeEnd;
54}
55
56bool IsDungeonAddress(uint32_t addr) {
57 return addr == kDungeonCrystals || addr == kDungeonPendants;
58}
59
60bool IsInItemsRange(uint32_t addr) {
61 return addr >= kItemsRangeStart && addr < kItemsRangeEnd;
62}
63
64} // namespace
65
75
77
79 return client_ && client_->IsConnected();
80}
81
83 if (!client_) {
85 }
86 auto status = client_->Connect();
87 if (!status.ok()) {
88 connection_error_ = std::string(status.message());
89 } else {
90 connection_error_.clear();
93 }
94}
95
96void SramViewerPanel::ConnectToPath(const std::string& socket_path) {
97 if (!client_) {
99 }
100 auto status = client_->Connect(socket_path);
101 if (!status.ok()) {
102 connection_error_ = std::string(status.message());
103 } else {
104 connection_error_.clear();
107 }
108}
109
111 if (client_) {
112 client_->Disconnect();
113 }
114}
115
118 if (!socket_paths_.empty()) {
119 if (selected_socket_index_ < 0 ||
120 selected_socket_index_ >= static_cast<int>(socket_paths_.size())) {
122 }
123 if (socket_path_buffer_[0] == '\0') {
124 std::snprintf(socket_path_buffer_, sizeof(socket_path_buffer_), "%s",
126 }
127 } else {
129 }
130}
131
133 variables_.clear();
134 variables_loaded_ = false;
135
136 if (!project_)
137 return;
139 return;
140
142 variables_loaded_ = true;
143
144 // Sort by address for consistent display
145 std::sort(variables_.begin(), variables_.end(),
146 [](const core::SramVariable& a, const core::SramVariable& b) {
147 return a.address < b.address;
148 });
149}
150
152 if (!IsConnected())
153 return;
154 if (variables_.empty())
155 return;
156
157 // Save previous values for change detection
159
160 // Read each variable individually (they may be scattered across WRAM)
161 float current_time = static_cast<float>(ImGui::GetTime());
162 for (const auto& var : variables_) {
163 auto result = client_->ReadByte(var.address);
164 if (result.ok()) {
165 uint8_t new_value = *result;
166
167 // Detect changes
168 auto prev_it = previous_values_.find(var.address);
169 if (prev_it != previous_values_.end() && prev_it->second != new_value) {
170 change_timestamps_[var.address] = current_time;
171 }
172
173 current_values_[var.address] = new_value;
174 }
175 }
176}
177
178void SramViewerPanel::PokeValue(uint32_t address, uint8_t value) {
179 if (!IsConnected())
180 return;
181
182 auto status = client_->WriteByte(address, value);
183 if (!status.ok()) {
184 status_message_ = absl::StrFormat("Write failed: %s", status.message());
185 } else {
186 status_message_ = 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", ICON_MD_MEMORY);
253
254 // Connection status indicator
255 ImGui::SameLine(ImGui::GetWindowWidth() - 100);
256 if (IsConnected()) {
257 float pulse = 0.7f + 0.3f * std::sin(ImGui::GetTime() * 2.0f);
258 ImVec4 connected_color = ImVec4(0.1f, pulse, 0.3f, 1.0f);
259 ImGui::TextColored(connected_color, "%s Connected", ICON_MD_CHECK_CIRCLE);
260 } else {
261 ImGui::TextColored(theme.status_error, "%s Disconnected", ICON_MD_ERROR);
262 }
263
264 ImGui::Separator();
265
266 if (!IsConnected()) {
267 ImGui::TextDisabled("Socket");
268 const char* preview =
270 selected_socket_index_ < static_cast<int>(socket_paths_.size()))
272 : "No sockets found";
273 ImGui::SetNextItemWidth(-40);
274 if (ImGui::BeginCombo("##sram_socket_combo", preview)) {
275 for (int i = 0; i < static_cast<int>(socket_paths_.size()); ++i) {
276 bool selected = (i == selected_socket_index_);
277 if (ImGui::Selectable(socket_paths_[i].c_str(), selected)) {
279 std::snprintf(socket_path_buffer_, sizeof(socket_path_buffer_), "%s",
280 socket_paths_[i].c_str());
281 }
282 if (selected) {
283 ImGui::SetItemDefaultFocus();
284 }
285 }
286 ImGui::EndCombo();
287 }
288 ImGui::SameLine();
289 if (ImGui::SmallButton(ICON_MD_REFRESH "##sram_refresh")) {
291 }
292
293 ImGui::TextDisabled("Path");
294 ImGui::SetNextItemWidth(-1);
295 ImGui::InputTextWithHint("##sram_socket_path",
296 "tcp://127.0.0.1:27015 or /tmp/mesen2-*.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 = absl::StrFormat(
394 "%s Story (%zu)", ICON_MD_AUTO_STORIES, story_vars.size());
395 if (ImGui::CollapsingHeader(
396 story_label.c_str(),
397 story_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
398 story_expanded_ = true;
399 for (const auto* var : story_vars) {
400 DrawVariableRow(*var);
401 }
402 } else {
403 story_expanded_ = false;
404 }
405 }
406
407 // Dungeon section
408 if (!dungeon_vars.empty()) {
409 std::string dungeon_label = absl::StrFormat(
410 "%s Dungeon (%zu)", ICON_MD_CASTLE, dungeon_vars.size());
411 if (ImGui::CollapsingHeader(
412 dungeon_label.c_str(),
413 dungeon_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
414 dungeon_expanded_ = true;
415 for (const auto* var : dungeon_vars) {
416 DrawVariableRow(*var);
417 }
418 } else {
419 dungeon_expanded_ = false;
420 }
421 }
422
423 // Items section
424 if (!item_vars.empty()) {
425 std::string items_label = absl::StrFormat(
426 "%s Items (%zu)", ICON_MD_INVENTORY_2, item_vars.size());
427 if (ImGui::CollapsingHeader(
428 items_label.c_str(),
429 items_expanded_ ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
430 items_expanded_ = true;
431 for (const auto* var : item_vars) {
432 DrawVariableRow(*var);
433 }
434 } else {
435 items_expanded_ = false;
436 }
437 }
438
439 // Other/uncategorized
440 if (!other_vars.empty()) {
441 std::string other_label = absl::StrFormat(
442 "%s Other (%zu)", ICON_MD_MORE_HORIZ, other_vars.size());
443 if (ImGui::CollapsingHeader(other_label.c_str())) {
444 for (const auto* var : other_vars) {
445 DrawVariableRow(*var);
446 }
447 }
448 }
449}
450
452 const auto& theme = AgentUI::GetTheme();
453 float current_time = static_cast<float>(ImGui::GetTime());
454
455 ImGui::PushID(static_cast<int>(var.address));
456
457 // Check if this value recently changed
458 bool recently_changed = false;
459 auto ts_it = change_timestamps_.find(var.address);
460 if (ts_it != change_timestamps_.end()) {
461 float elapsed = current_time - ts_it->second;
462 if (elapsed < kChangeHighlightDuration) {
463 recently_changed = true;
464 // Flash yellow background that fades out
465 float alpha = 1.0f - (elapsed / kChangeHighlightDuration);
466 ImVec4 highlight_color = ImVec4(0.9f, 0.8f, 0.1f, alpha * 0.3f);
467 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
468 ImVec2 row_size = ImVec2(ImGui::GetContentRegionAvail().x,
469 ImGui::GetTextLineHeightWithSpacing());
470 ImGui::GetWindowDrawList()->AddRectFilled(
471 cursor_pos,
472 ImVec2(cursor_pos.x + row_size.x, cursor_pos.y + row_size.y),
473 ImGui::ColorConvertFloat4ToU32(highlight_color));
474 }
475 }
476
477 // Address column
478 ImGui::TextColored(theme.text_secondary_color, "$%06X", var.address);
479
480 // Name column
481 ImGui::SameLine(80);
482 if (recently_changed) {
483 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.2f, 1.0f), "%s", var.name.c_str());
484 } else {
485 ImGui::Text("%s", var.name.c_str());
486 }
487
488 // Purpose (tooltip)
489 if (!var.purpose.empty() && ImGui::IsItemHovered()) {
490 ImGui::SetTooltip("%s", var.purpose.c_str());
491 }
492
493 // Value columns (only when connected and we have data)
494 auto val_it = current_values_.find(var.address);
495 if (val_it != current_values_.end()) {
496 uint8_t value = val_it->second;
497
498 // Decimal value
499 ImGui::SameLine(240);
500 ImGui::Text("%d", value);
501
502 // Hex value
503 ImGui::SameLine(290);
504 ImGui::TextColored(theme.text_secondary_color, "$%02X", value);
505
506 // Edit button
507 ImGui::SameLine(340);
508 std::string edit_label =
509 absl::StrFormat("%s##edit_%06X", ICON_MD_EDIT, var.address);
510 if (ImGui::SmallButton(edit_label.c_str())) {
511 editing_active_ = true;
513 editing_value_ = value;
514 ImGui::OpenPopup("SramEditPopup");
515 }
516 }
517
518 // Special expansions for key addresses
519 if (val_it != current_values_.end()) {
520 if (var.address == kDungeonCrystals) {
521 DrawCrystalBitfield(val_it->second, var.address);
522 } else if (var.address == kStoryRangeStart) {
523 // GameState is the first story address ($7EF3C5)
524 DrawGameStateDropdown(val_it->second, var.address);
525 }
526 }
527
528 ImGui::PopID();
529}
530
531void SramViewerPanel::DrawCrystalBitfield(uint8_t value, uint32_t address) {
532 ImGui::Indent(20);
533 bool any_changed = false;
534 uint8_t new_value = value;
535
536 for (const auto& bit : kCrystalBits) {
537 bool set = (value & bit.mask) != 0;
538 std::string cb_label =
539 absl::StrFormat("%s##crystal_%02X", bit.label, bit.mask);
540 if (ImGui::Checkbox(cb_label.c_str(), &set)) {
541 if (set) {
542 new_value |= bit.mask;
543 } else {
544 new_value &= ~bit.mask;
545 }
546 any_changed = true;
547 }
548 }
549
550 if (any_changed) {
551 PokeValue(address, new_value);
552 }
553
554 ImGui::Unindent(20);
555}
556
557void SramViewerPanel::DrawGameStateDropdown(uint8_t value, uint32_t address) {
558 ImGui::Indent(20);
559
560 int current_index = value;
561 if (current_index >= kGameStateLabelCount) {
562 current_index = -1; // Unknown state
563 }
564
565 const char* preview =
566 (current_index >= 0 && current_index < kGameStateLabelCount)
567 ? kGameStateLabels[current_index]
568 : "Unknown";
569
570 ImGui::SetNextItemWidth(200);
571 if (ImGui::BeginCombo("##gamestate_combo", preview)) {
572 for (int i = 0; i < kGameStateLabelCount; ++i) {
573 bool selected = (i == current_index);
574 if (ImGui::Selectable(kGameStateLabels[i], selected)) {
575 PokeValue(address, static_cast<uint8_t>(i));
576 }
577 if (selected) {
578 ImGui::SetItemDefaultFocus();
579 }
580 }
581 ImGui::EndCombo();
582 }
583
584 ImGui::Unindent(20);
585}
586
587} // namespace editor
588} // 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:212