8#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
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"},
86 auto status =
client_->Connect();
100 auto status =
client_->Connect(socket_path);
146 [](
const core::SramVariable& a,
const core::SramVariable& b) {
147 return a.address < b.address;
161 float current_time =
static_cast<float>(ImGui::GetTime());
163 auto result =
client_->ReadByte(var.address);
165 uint8_t new_value = *result;
182 auto status =
client_->WriteByte(address, value);
184 status_message_ = absl::StrFormat(
"Write failed: %s", status.message());
186 status_message_ = absl::StrFormat(
"Wrote $%02X to $%06X", value, address);
193 ImGui::PushID(
"SramViewerPanel");
210 if (ImGui::BeginChild(
"SramViewer_Panel", ImVec2(0, 0),
true)) {
211 if (ImGui::IsWindowAppearing()) {
224 "No SRAM variables loaded. Ensure hack_manifest.json is present "
228 ImGui::TextDisabled(
"Filter");
230 ImGui::SetNextItemWidth(-1);
231 ImGui::InputTextWithHint(
"##sram_filter",
"Search by name or address",
240 ImGui::TextDisabled(
"Connect to Mesen2 to see live values.");
252 ImGui::TextColored(theme.accent_color,
"%s SRAM Viewer",
ICON_MD_MEMORY);
255 ImGui::SameLine(ImGui::GetWindowWidth() - 100);
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);
261 ImGui::TextColored(theme.status_error,
"%s Disconnected",
ICON_MD_ERROR);
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) {
283 ImGui::SetItemDefaultFocus();
293 ImGui::TextDisabled(
"Path");
294 ImGui::SetNextItemWidth(-1);
295 ImGui::InputTextWithHint(
"##sram_socket_path",
"/tmp/mesen2-12345.sock",
326 ImGui::SetNextItemWidth(60);
339 ImGui::TextColored(theme.text_secondary_color,
"%s",
346 std::vector<const core::SramVariable*> story_vars;
347 std::vector<const core::SramVariable*> dungeon_vars;
348 std::vector<const core::SramVariable*> item_vars;
349 std::vector<const core::SramVariable*> other_vars;
351 std::string filter_lower;
354 std::transform(filter_lower.begin(), filter_lower.end(),
355 filter_lower.begin(), ::tolower);
360 if (!filter_lower.empty()) {
361 std::string name_lower = var.name;
362 std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
364 std::string purpose_lower = var.purpose;
365 std::transform(purpose_lower.begin(), purpose_lower.end(),
366 purpose_lower.begin(), ::tolower);
367 std::string addr_str = absl::StrFormat(
"$%06X", var.address);
368 std::string addr_lower = addr_str;
369 std::transform(addr_lower.begin(), addr_lower.end(), addr_lower.begin(),
372 if (name_lower.find(filter_lower) == std::string::npos &&
373 purpose_lower.find(filter_lower) == std::string::npos &&
374 addr_lower.find(filter_lower) == std::string::npos) {
380 story_vars.push_back(&var);
382 dungeon_vars.push_back(&var);
384 item_vars.push_back(&var);
386 other_vars.push_back(&var);
391 if (!story_vars.empty()) {
392 std::string story_label = absl::StrFormat(
394 if (ImGui::CollapsingHeader(
398 for (
const auto* var : story_vars) {
407 if (!dungeon_vars.empty()) {
408 std::string dungeon_label = absl::StrFormat(
410 if (ImGui::CollapsingHeader(
411 dungeon_label.c_str(),
414 for (
const auto* var : dungeon_vars) {
423 if (!item_vars.empty()) {
424 std::string items_label = absl::StrFormat(
426 if (ImGui::CollapsingHeader(
430 for (
const auto* var : item_vars) {
439 if (!other_vars.empty()) {
440 std::string other_label = absl::StrFormat(
442 if (ImGui::CollapsingHeader(other_label.c_str())) {
443 for (
const auto* var : other_vars) {
452 float current_time =
static_cast<float>(ImGui::GetTime());
454 ImGui::PushID(
static_cast<int>(var.address));
457 bool recently_changed =
false;
460 float elapsed = current_time - ts_it->second;
461 if (elapsed < kChangeHighlightDuration) {
462 recently_changed =
true;
465 ImVec4 highlight_color = ImVec4(0.9f, 0.8f, 0.1f, alpha * 0.3f);
466 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
467 ImVec2 row_size = ImVec2(ImGui::GetContentRegionAvail().x,
468 ImGui::GetTextLineHeightWithSpacing());
469 ImGui::GetWindowDrawList()->AddRectFilled(
471 ImVec2(cursor_pos.x + row_size.x, cursor_pos.y + row_size.y),
472 ImGui::ColorConvertFloat4ToU32(highlight_color));
477 ImGui::TextColored(theme.text_secondary_color,
"$%06X", var.address);
481 if (recently_changed) {
482 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.2f, 1.0f),
"%s", var.name.c_str());
484 ImGui::Text(
"%s", var.name.c_str());
488 if (!var.purpose.empty() && ImGui::IsItemHovered()) {
489 ImGui::SetTooltip(
"%s", var.purpose.c_str());
495 uint8_t value = val_it->second;
498 ImGui::SameLine(240);
499 ImGui::Text(
"%d", value);
502 ImGui::SameLine(290);
503 ImGui::TextColored(theme.text_secondary_color,
"$%02X", value);
506 ImGui::SameLine(340);
507 std::string edit_label =
508 absl::StrFormat(
"%s##edit_%06X",
ICON_MD_EDIT, var.address);
509 if (ImGui::SmallButton(edit_label.c_str())) {
513 ImGui::OpenPopup(
"SramEditPopup");
519 if (var.address == kDungeonCrystals) {
521 }
else if (var.address == kStoryRangeStart) {
532 bool any_changed =
false;
533 uint8_t new_value = value;
536 bool set = (value & bit.mask) != 0;
537 std::string cb_label =
538 absl::StrFormat(
"%s##crystal_%02X", bit.label, bit.mask);
539 if (ImGui::Checkbox(cb_label.c_str(), &set)) {
541 new_value |= bit.mask;
543 new_value &= ~bit.mask;
559 int current_index = value;
560 if (current_index >= kGameStateLabelCount) {
564 const char* preview =
566 ? kGameStateLabels[current_index]
569 ImGui::SetNextItemWidth(200);
570 if (ImGui::BeginCombo(
"##gamestate_combo", preview)) {
572 bool selected = (i == current_index);
573 if (ImGui::Selectable(kGameStateLabels[i], selected)) {
574 PokeValue(address,
static_cast<uint8_t
>(i));
577 ImGui::SetItemDefaultFocus();
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)
float time_since_refresh_
void DrawGameStateDropdown(uint8_t value, uint32_t address)
std::vector< std::string > socket_paths_
project::YazeProject * project_
std::string status_message_
void PokeValue(uint32_t address, uint8_t value)
void LoadVariablesFromManifest()
std::vector< core::SramVariable > variables_
char socket_path_buffer_[256]
std::unordered_map< uint32_t, uint8_t > previous_values_
void DrawConnectionHeader()
std::unordered_map< uint32_t, uint8_t > current_values_
uint32_t editing_address_
std::unordered_map< uint32_t, float > change_timestamps_
void ConnectToPath(const std::string &socket_path)
std::string connection_error_
int selected_socket_index_
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_MORE_HORIZ
#define ICON_MD_AUTO_MODE
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_AUTO_STORIES
#define ICON_MD_INVENTORY_2
const AgentUITheme & GetTheme()
bool IsDungeonAddress(uint32_t addr)
constexpr float kChangeHighlightDuration
constexpr uint32_t kItemsRangeStart
constexpr uint32_t kStoryRangeStart
bool IsInItemsRange(uint32_t addr)
const char * kGameStateLabels[]
constexpr uint32_t kDungeonCrystals
constexpr int kGameStateLabelCount
bool IsInStoryRange(uint32_t addr)
constexpr uint32_t kStoryRangeEnd
constexpr CrystalBit kCrystalBits[]
constexpr uint32_t kItemsRangeEnd
constexpr uint32_t kDungeonPendants
core::HackManifest hack_manifest