yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
mesen_socket_client.cc
Go to the documentation of this file.
2
3#include <fcntl.h>
4#include <sys/stat.h>
5#include <filesystem>
6
7#ifdef _WIN32
8// clang-format off
9// winsock2.h must precede afunix.h (defines ADDRESS_FAMILY)
10#include <winsock2.h>
11#include <ws2tcpip.h>
12#include <afunix.h>
13#include <io.h>
14// clang-format on
15#define close closesocket
16typedef int ssize_t;
17#else
18#include <sys/socket.h>
19#include <sys/un.h>
20#include <unistd.h>
21#endif
22
23#include <cstdlib>
24
25#include <cerrno>
26#include <cstring>
27#include <regex>
28#include <sstream>
29
30#include "absl/strings/numbers.h"
31#include "absl/strings/str_cat.h"
32#include "absl/strings/str_format.h"
33#include "absl/strings/str_split.h"
34
35#include <charconv>
36
37namespace yaze {
38namespace emu {
39namespace mesen {
40
41namespace {
42
43// Simple JSON value extraction (avoids pulling in a full JSON library)
44std::string ExtractJsonString(const std::string& json, const std::string& key) {
45 std::string search = "\"" + key + "\":";
46 size_t pos = json.find(search);
47 if (pos == std::string::npos)
48 return "";
49
50 pos += search.length();
51 while (pos < json.length() && (json[pos] == ' ' || json[pos] == '\t'))
52 pos++;
53
54 if (pos >= json.length())
55 return "";
56
57 if (json[pos] == '"') {
58 // String value
59 size_t start = pos + 1;
60 size_t end = json.find('"', start);
61 while (end != std::string::npos && end > 0 && json[end - 1] == '\\') {
62 end = json.find('"', end + 1);
63 }
64 if (end == std::string::npos)
65 return "";
66 return json.substr(start, end - start);
67 } else if (json[pos] == '{') {
68 // Object value - find matching brace
69 int depth = 1;
70 size_t start = pos;
71 pos++;
72 while (pos < json.length() && depth > 0) {
73 if (json[pos] == '{')
74 depth++;
75 else if (json[pos] == '}')
76 depth--;
77 pos++;
78 }
79 return json.substr(start, pos - start);
80 } else if (json[pos] == '[') {
81 // Array value - find matching bracket
82 int depth = 1;
83 size_t start = pos;
84 pos++;
85 while (pos < json.length() && depth > 0) {
86 if (json[pos] == '[')
87 depth++;
88 else if (json[pos] == ']')
89 depth--;
90 pos++;
91 }
92 return json.substr(start, pos - start);
93 } else {
94 // Number or boolean
95 size_t start = pos;
96 while (pos < json.length() && json[pos] != ',' && json[pos] != '}' &&
97 json[pos] != ']') {
98 pos++;
99 }
100 return json.substr(start, pos - start);
101 }
102}
103
104int64_t ExtractJsonInt(const std::string& json, const std::string& key,
105 int64_t default_value = 0) {
106 std::string value = ExtractJsonString(json, key);
107 if (value.empty())
108 return default_value;
109
110 // Handle hex strings like "0x7E0000"
111 if (value.length() > 2 && value[0] == '0' &&
112 (value[1] == 'x' || value[1] == 'X')) {
113 int64_t result;
114 std::string hex_str = value.substr(2);
115 auto [ptr, ec] = std::from_chars(
116 hex_str.data(), hex_str.data() + hex_str.size(), result, 16);
117 if (ec == std::errc()) {
118 return result;
119 }
120 }
121
122 int64_t result;
123 if (absl::SimpleAtoi(value, &result)) {
124 return result;
125 }
126 return default_value;
127}
128
129double ExtractJsonDouble(const std::string& json, const std::string& key,
130 double default_value = 0.0) {
131 std::string value = ExtractJsonString(json, key);
132 if (value.empty())
133 return default_value;
134
135 double result;
136 if (absl::SimpleAtod(value, &result)) {
137 return result;
138 }
139 return default_value;
140}
141
142bool ExtractJsonBool(const std::string& json, const std::string& key,
143 bool default_value = false) {
144 std::string value = ExtractJsonString(json, key);
145 if (value.empty())
146 return default_value;
147 return value == "true";
148}
149
150std::string BuildJsonCommand(const std::string& type) {
151 return absl::StrFormat("{\"type\":\"%s\"}\n", type);
152}
153
154std::string EscapeJsonValue(const std::string& value) {
155 std::string escaped;
156 escaped.reserve(value.size());
157 for (char c : value) {
158 switch (c) {
159 case '\\':
160 escaped += "\\\\";
161 break;
162 case '"':
163 escaped += "\\\"";
164 break;
165 case '\n':
166 escaped += "\\n";
167 break;
168 case '\r':
169 escaped += "\\r";
170 break;
171 case '\t':
172 escaped += "\\t";
173 break;
174 default:
175 escaped += c;
176 break;
177 }
178 }
179 return escaped;
180}
181
183 const std::string& type,
184 const std::vector<std::pair<std::string, std::string>>& params) {
185 std::stringstream ss;
186 ss << "{\"type\":\"" << type << "\"";
187 for (const auto& [key, value] : params) {
188 ss << ",\"" << key << "\":\"" << EscapeJsonValue(value) << "\"";
189 }
190 ss << "}\n";
191 return ss.str();
192}
193
194constexpr size_t kMaxResponseSize = 4 * 1024 * 1024;
195
196absl::Status ConnectSocketToPath(const std::string& socket_path,
197 int* socket_fd) {
198 if (socket_fd == nullptr) {
199 return absl::InvalidArgumentError("socket_fd output pointer is null");
200 }
201
202 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
203 if (fd < 0) {
204 return absl::InternalError(
205 absl::StrCat("Failed to create socket: ", strerror(errno)));
206 }
207
208 struct sockaddr_un addr;
209 memset(&addr, 0, sizeof(addr));
210 addr.sun_family = AF_UNIX;
211 strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
212
213 if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
214 close(fd);
215 return absl::UnavailableError(absl::StrCat(
216 "Failed to connect to ", socket_path, ": ", strerror(errno)));
217 }
218
219 *socket_fd = fd;
220 return absl::OkStatus();
221}
222
223void ShutdownSocketFd(int fd) {
224 if (fd < 0) {
225 return;
226 }
227#ifdef _WIN32
228 shutdown(fd, SD_BOTH);
229#else
230 shutdown(fd, SHUT_RDWR);
231#endif
232}
233
234} // namespace
235
237
241
243 auto paths = FindSocketPaths();
244 if (paths.empty()) {
245 return absl::NotFoundError(
246 "No Mesen2 socket found. Is Mesen2-OoS running?");
247 }
248 return Connect(paths[0]);
249}
250
251absl::Status MesenSocketClient::Connect(const std::string& socket_path) {
252 if (IsConnected()) {
253 Disconnect();
254 }
255
256 int fd = -1;
257 auto connect_status = ConnectSocketToPath(socket_path, &fd);
258 if (!connect_status.ok()) {
259 return connect_status;
260 }
261
262 socket_fd_ = fd;
263 socket_path_ = socket_path;
264 connected_ = true;
265
266 // Verify connection with a ping
267 auto status = Ping();
268 if (!status.ok()) {
269 Disconnect();
270 return status;
271 }
272
273 return absl::OkStatus();
274}
275
277 // Best-effort: stop event stream first so background thread exits cleanly.
278 (void)Unsubscribe();
279
280 if (socket_fd_ >= 0) {
281 close(socket_fd_);
282 socket_fd_ = -1;
283 }
284 socket_path_.clear();
285 connected_ = false;
286}
287
289 return connected_;
290}
291
292std::vector<std::string> MesenSocketClient::FindSocketPaths() {
293 const char* env_path = std::getenv("MESEN2_SOCKET_PATH");
294 if (env_path && env_path[0] != '\0') {
295#ifdef _WIN32
296 // Windows AF_UNIX sockets don't report S_IFSOCK via stat; trust the env var
297 if (std::filesystem::exists(env_path)) {
298 return {std::string(env_path)};
299 }
300#else
301 struct stat st;
302 if (stat(env_path, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) {
303 return {std::string(env_path)};
304 }
305#endif
306 }
307
308 std::vector<std::string> paths;
309 namespace fs = std::filesystem;
310 std::vector<fs::path> search_paths;
311
312#ifdef _WIN32
313 search_paths.push_back(fs::temp_directory_path());
314#else
315 search_paths.push_back("/tmp");
316#endif
317
318 std::regex socket_pattern("mesen2-\\d+\\.sock");
319
320 for (const auto& search_path : search_paths) {
321 std::error_code ec;
322 if (!fs::exists(search_path, ec))
323 continue;
324
325 for (const auto& entry : fs::directory_iterator(search_path, ec)) {
326 if (ec)
327 break;
328 // On Windows, checking is_socket might be unreliable or not supported for AF_UNIX files,
329 // so we mainly rely on the filename pattern.
330 std::string filename = entry.path().filename().string();
331 if (std::regex_match(filename, socket_pattern)) {
332 paths.push_back(entry.path().string());
333 }
334 }
335 }
336
337 return paths;
338}
339
340std::vector<std::string> MesenSocketClient::ListAvailableSockets() {
341 return FindSocketPaths();
342}
343
344absl::StatusOr<std::string> MesenSocketClient::SendCommandOnSocket(
345 int fd, const std::string& json, bool update_connection_state) {
346 if (fd < 0) {
347 return absl::FailedPreconditionError("Socket is not connected");
348 }
349
350 // Send command
351 ssize_t sent = send(fd, json.c_str(), json.length(), 0);
352 if (sent < 0) {
353 if (update_connection_state) {
354 connected_ = false;
355 }
356 return absl::InternalError(
357 absl::StrCat("Failed to send command: ", strerror(errno)));
358 }
359
360 // Receive response (with timeout)
361 struct timeval tv;
362 tv.tv_sec = 5;
363 tv.tv_usec = 0;
364 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&tv),
365 sizeof(tv));
366
367 std::string response;
368 char buffer[4096];
369 while (true) {
370 ssize_t received = recv(fd, buffer, sizeof(buffer), 0);
371 if (received < 0) {
372#ifdef _WIN32
373 int err = WSAGetLastError();
374 if (err == WSAEWOULDBLOCK || err == WSAETIMEDOUT) {
375#else
376 if (errno == EAGAIN || errno == EWOULDBLOCK) {
377#endif
378 if (response.empty()) {
379 return absl::DeadlineExceededError("Timeout waiting for response");
380 }
381 break;
382 }
383 if (update_connection_state) {
384 connected_ = false;
385 }
386 return absl::InternalError(
387 absl::StrCat("Failed to receive response: ", strerror(errno)));
388 }
389 if (received == 0) {
390 break;
391 }
392 response.append(buffer, static_cast<size_t>(received));
393 if (response.size() > kMaxResponseSize) {
394 return absl::ResourceExhaustedError("Mesen2 response too large");
395 }
396 if (response.find('\n') != std::string::npos) {
397 break;
398 }
399 }
400 if (response.empty()) {
401 return absl::DeadlineExceededError("Empty response from Mesen2");
402 }
403 auto newline_pos = response.find('\n');
404 if (newline_pos != std::string::npos) {
405 response = response.substr(0, newline_pos);
406 }
407
408 return ParseResponse(response);
409}
410
411absl::StatusOr<std::string> MesenSocketClient::SendCommand(
412 const std::string& json) {
413 if (!IsConnected()) {
414 return absl::FailedPreconditionError("Not connected to Mesen2");
415 }
416
417 std::lock_guard<std::mutex> lock(command_mutex_);
418 return SendCommandOnSocket(socket_fd_, json, /*update_connection_state=*/true);
419}
420
421absl::StatusOr<std::string> MesenSocketClient::ParseResponse(
422 const std::string& response) {
423 bool success = ExtractJsonBool(response, "success");
424 if (!success) {
425 std::string error = ExtractJsonString(response, "error");
426 if (error.empty())
427 error = "Unknown Mesen2 error";
428 return absl::InternalError(error);
429 }
430
431 std::string data = ExtractJsonString(response, "data");
432 return data.empty() ? response : data;
433}
434
435// ─────────────────────────────────────────────────────────────────────────────
436// Control Commands
437// ─────────────────────────────────────────────────────────────────────────────
438
440 auto result = SendCommand(BuildJsonCommand("PING"));
441 if (!result.ok())
442 return result.status();
443 return absl::OkStatus();
444}
445
446absl::StatusOr<MesenState> MesenSocketClient::GetState() {
447 auto result = SendCommand(BuildJsonCommand("STATE"));
448 if (!result.ok())
449 return result.status();
450
451 MesenState state;
452 state.running = ExtractJsonBool(*result, "running");
453 state.paused = ExtractJsonBool(*result, "paused");
454 state.debugging = ExtractJsonBool(*result, "debugging");
455 state.frame = ExtractJsonInt(*result, "frame");
456 state.fps = ExtractJsonDouble(*result, "fps");
457 state.console_type = ExtractJsonInt(*result, "consoleType");
458 return state;
459}
460
462 auto result = SendCommand(BuildJsonCommand("PAUSE"));
463 return result.status();
464}
465
467 auto result = SendCommand(BuildJsonCommand("RESUME"));
468 return result.status();
469}
470
472 auto result = SendCommand(BuildJsonCommand("RESET"));
473 return result.status();
474}
475
477 auto result = SendCommand(BuildJsonCommand("FRAME"));
478 return result.status();
479}
480
481absl::Status MesenSocketClient::Step(int count, const std::string& mode) {
482 auto result = SendCommand(BuildJsonCommand(
483 "STEP", {{"count", std::to_string(count)}, {"mode", mode}}));
484 return result.status();
485}
486
487// ─────────────────────────────────────────────────────────────────────────────
488// Input Commands
489// ─────────────────────────────────────────────────────────────────────────────
490
492 bool pressed) {
493 current_input_.SetButton(button, pressed);
495}
496
498 const emu::input::ControllerState& state) {
499 current_input_ = state;
500
501 std::vector<std::string> buttons;
502 if (state.IsPressed(emu::input::SnesButton::A)) buttons.push_back("a");
503 if (state.IsPressed(emu::input::SnesButton::B)) buttons.push_back("b");
504 if (state.IsPressed(emu::input::SnesButton::X)) buttons.push_back("x");
505 if (state.IsPressed(emu::input::SnesButton::Y)) buttons.push_back("y");
506 if (state.IsPressed(emu::input::SnesButton::L)) buttons.push_back("l");
507 if (state.IsPressed(emu::input::SnesButton::R)) buttons.push_back("r");
509 buttons.push_back("select");
511 buttons.push_back("start");
512 if (state.IsPressed(emu::input::SnesButton::UP)) buttons.push_back("up");
513 if (state.IsPressed(emu::input::SnesButton::DOWN)) buttons.push_back("down");
514 if (state.IsPressed(emu::input::SnesButton::LEFT)) buttons.push_back("left");
515 if (state.IsPressed(emu::input::SnesButton::RIGHT)) buttons.push_back("right");
516
517 std::string buttons_str;
518 for (size_t i = 0; i < buttons.size(); ++i) {
519 if (i > 0) buttons_str += ",";
520 buttons_str += buttons[i];
521 }
522
523 auto result = SendCommand(BuildJsonCommand("INPUT", {{"buttons", buttons_str}}));
524 return result.status();
525}
526
527// ─────────────────────────────────────────────────────────────────────────────
528// Memory Commands
529// ─────────────────────────────────────────────────────────────────────────────
530
531absl::StatusOr<uint8_t> MesenSocketClient::ReadByte(uint32_t addr) {
532 auto result = SendCommand(
533 BuildJsonCommand("READ", {{"addr", absl::StrFormat("0x%06X", addr)}}));
534 if (!result.ok())
535 return result.status();
536 return static_cast<uint8_t>(ExtractJsonInt(*result, "data", 0));
537}
538
539absl::StatusOr<uint16_t> MesenSocketClient::ReadWord(uint32_t addr) {
540 auto result = SendCommand(
541 BuildJsonCommand("READ16", {{"addr", absl::StrFormat("0x%06X", addr)}}));
542 if (!result.ok())
543 return result.status();
544 return static_cast<uint16_t>(ExtractJsonInt(*result, "data", 0));
545}
546
547absl::StatusOr<std::vector<uint8_t>> MesenSocketClient::ReadBlock(uint32_t addr,
548 size_t len) {
549 auto result = SendCommand(
550 BuildJsonCommand("READBLOCK", {{"addr", absl::StrFormat("0x%06X", addr)},
551 {"len", std::to_string(len)}}));
552 if (!result.ok())
553 return result.status();
554
555 // Response is hex string
556 std::string hex = ExtractJsonString(*result, "data");
557 if (hex.empty()) {
558 // Try raw data field
559 hex = *result;
560 }
561
562 std::vector<uint8_t> data;
563 data.reserve(len);
564
565 for (size_t i = 0; i + 1 < hex.length(); i += 2) {
566 int byte;
567 std::string byte_hex = hex.substr(i, 2);
568 auto [ptr, ec] = std::from_chars(
569 byte_hex.data(), byte_hex.data() + byte_hex.size(), byte, 16);
570 if (ec == std::errc()) {
571 data.push_back(static_cast<uint8_t>(byte));
572 }
573 }
574 return data;
575}
576
577absl::Status MesenSocketClient::WriteByte(uint32_t addr, uint8_t value) {
578 auto result = SendCommand(
579 BuildJsonCommand("WRITE", {{"addr", absl::StrFormat("0x%06X", addr)},
580 {"value", absl::StrFormat("0x%02X", value)}}));
581 return result.status();
582}
583
584absl::Status MesenSocketClient::WriteWord(uint32_t addr, uint16_t value) {
585 auto result = SendCommand(BuildJsonCommand(
586 "WRITE16", {{"addr", absl::StrFormat("0x%06X", addr)},
587 {"value", absl::StrFormat("0x%04X", value)}}));
588 return result.status();
589}
590
591absl::Status MesenSocketClient::WriteBlock(uint32_t addr,
592 const std::vector<uint8_t>& data) {
593 std::stringstream hex;
594 for (uint8_t byte : data) {
595 hex << absl::StrFormat("%02X", byte);
596 }
597 auto result = SendCommand(BuildJsonCommand(
598 "WRITEBLOCK",
599 {{"addr", absl::StrFormat("0x%06X", addr)}, {"hex", hex.str()}}));
600 return result.status();
601}
602
603// ─────────────────────────────────────────────────────────────────────────────
604// Debugging Commands
605// ─────────────────────────────────────────────────────────────────────────────
606
607absl::StatusOr<CpuState> MesenSocketClient::GetCpuState() {
608 auto result = SendCommand(BuildJsonCommand("CPU"));
609 if (!result.ok())
610 return result.status();
611
612 CpuState state;
613 state.A = static_cast<uint16_t>(ExtractJsonInt(*result, "A"));
614 state.X = static_cast<uint16_t>(ExtractJsonInt(*result, "X"));
615 state.Y = static_cast<uint16_t>(ExtractJsonInt(*result, "Y"));
616 state.SP = static_cast<uint16_t>(ExtractJsonInt(*result, "SP"));
617 state.D = static_cast<uint16_t>(ExtractJsonInt(*result, "D"));
618 state.PC = static_cast<uint32_t>(ExtractJsonInt(*result, "PC"));
619 state.K = static_cast<uint8_t>(ExtractJsonInt(*result, "K"));
620 state.DBR = static_cast<uint8_t>(ExtractJsonInt(*result, "DBR"));
621 state.P = static_cast<uint8_t>(ExtractJsonInt(*result, "P"));
622 state.emulation_mode = ExtractJsonBool(*result, "emulationMode");
623 return state;
624}
625
626absl::StatusOr<std::string> MesenSocketClient::Disassemble(uint32_t addr,
627 int count) {
628 auto result = SendCommand(
629 BuildJsonCommand("DISASM", {{"addr", absl::StrFormat("0x%06X", addr)},
630 {"count", std::to_string(count)}}));
631 if (!result.ok())
632 return result.status();
633 return *result;
634}
635
637 uint32_t addr, BreakpointType type, const std::string& condition) {
638 std::string type_str;
639 switch (type) {
641 type_str = "exec";
642 break;
644 type_str = "read";
645 break;
647 type_str = "write";
648 break;
650 type_str = "rw";
651 break;
652 }
653
654 std::vector<std::pair<std::string, std::string>> params = {
655 {"action", "add"},
656 {"addr", absl::StrFormat("0x%06X", addr)},
657 {"bptype", type_str}};
658
659 if (!condition.empty()) {
660 params.push_back({"condition", condition});
661 }
662
663 auto result = SendCommand(BuildJsonCommand("BREAKPOINT", params));
664 if (!result.ok())
665 return result.status();
666
667 return static_cast<int>(ExtractJsonInt(*result, "id", -1));
668}
669
671 auto result = SendCommand(BuildJsonCommand(
672 "BREAKPOINT", {{"action", "remove"}, {"id", std::to_string(id)}}));
673 return result.status();
674}
675
677 auto result =
678 SendCommand(BuildJsonCommand("BREAKPOINT", {{"action", "clear"}}));
679 return result.status();
680}
681
682absl::StatusOr<std::string> MesenSocketClient::GetTrace(int count) {
683 auto result = SendCommand(
684 BuildJsonCommand("TRACE", {{"count", std::to_string(count)}}));
685 if (!result.ok())
686 return result.status();
687 return *result;
688}
689
690// ─────────────────────────────────────────────────────────────────────────────
691// ALTTP Commands
692// ─────────────────────────────────────────────────────────────────────────────
693
694absl::StatusOr<GameState> MesenSocketClient::GetGameState() {
695 auto result = SendCommand(BuildJsonCommand("GAMESTATE"));
696 if (!result.ok())
697 return result.status();
698
699 GameState state;
700
701 // Parse link state
702 std::string link_json = ExtractJsonString(*result, "link");
703 state.link.x = static_cast<uint16_t>(ExtractJsonInt(link_json, "x"));
704 state.link.y = static_cast<uint16_t>(ExtractJsonInt(link_json, "y"));
705 state.link.layer = static_cast<uint8_t>(ExtractJsonInt(link_json, "layer"));
706 state.link.direction =
707 static_cast<uint8_t>(ExtractJsonInt(link_json, "direction"));
708 state.link.state = static_cast<uint8_t>(ExtractJsonInt(link_json, "state"));
709 state.link.pose = static_cast<uint8_t>(ExtractJsonInt(link_json, "pose"));
710
711 // Parse health
712 std::string health_json = ExtractJsonString(*result, "health");
713 state.items.current_health =
714 static_cast<uint8_t>(ExtractJsonInt(health_json, "current"));
715 state.items.max_health =
716 static_cast<uint8_t>(ExtractJsonInt(health_json, "max"));
717
718 // Parse items
719 std::string items_json = ExtractJsonString(*result, "items");
720 state.items.magic = static_cast<uint8_t>(ExtractJsonInt(items_json, "magic"));
721 state.items.rupees =
722 static_cast<uint16_t>(ExtractJsonInt(items_json, "rupees"));
723 state.items.bombs = static_cast<uint8_t>(ExtractJsonInt(items_json, "bombs"));
724 state.items.arrows =
725 static_cast<uint8_t>(ExtractJsonInt(items_json, "arrows"));
726
727 // Parse game mode
728 std::string game_json = ExtractJsonString(*result, "game");
729 state.game.mode = static_cast<uint8_t>(ExtractJsonInt(game_json, "mode"));
730 state.game.submode =
731 static_cast<uint8_t>(ExtractJsonInt(game_json, "submode"));
732 state.game.indoors = ExtractJsonBool(game_json, "indoors");
733 state.game.room_id =
734 static_cast<uint16_t>(ExtractJsonInt(game_json, "room_id"));
735 state.game.overworld_area =
736 static_cast<uint8_t>(ExtractJsonInt(game_json, "overworld_area"));
737
738 return state;
739}
740
741absl::StatusOr<std::vector<SpriteInfo>> MesenSocketClient::GetSprites(
742 bool all) {
743 std::vector<std::pair<std::string, std::string>> params;
744 if (all) {
745 params.push_back({"all", "true"});
746 }
747
748 auto result =
749 SendCommand(params.empty() ? BuildJsonCommand("SPRITES")
750 : BuildJsonCommand("SPRITES", params));
751 if (!result.ok())
752 return result.status();
753
754 std::vector<SpriteInfo> sprites;
755
756 // Find sprites array in response
757 size_t array_start = result->find("[");
758 size_t array_end = result->rfind("]");
759 if (array_start == std::string::npos || array_end == std::string::npos) {
760 return sprites;
761 }
762
763 std::string array_content =
764 result->substr(array_start + 1, array_end - array_start - 1);
765
766 // Parse each sprite object
767 size_t pos = 0;
768 while ((pos = array_content.find("{", pos)) != std::string::npos) {
769 size_t end = array_content.find("}", pos);
770 if (end == std::string::npos)
771 break;
772
773 std::string sprite_json = array_content.substr(pos, end - pos + 1);
774
775 SpriteInfo sprite;
776 sprite.slot = static_cast<int>(ExtractJsonInt(sprite_json, "slot"));
777 sprite.type = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "type"));
778 sprite.state = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "state"));
779 sprite.x = static_cast<uint16_t>(ExtractJsonInt(sprite_json, "x"));
780 sprite.y = static_cast<uint16_t>(ExtractJsonInt(sprite_json, "y"));
781 sprite.health = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "health"));
782 sprite.subtype =
783 static_cast<uint8_t>(ExtractJsonInt(sprite_json, "subtype"));
784
785 sprites.push_back(sprite);
786 pos = end + 1;
787 }
788
789 return sprites;
790}
791
793 const std::string& colmap) {
794 auto result = SendCommand(BuildJsonCommand(
795 "COLLISION_OVERLAY",
796 {{"action", enable ? "enable" : "disable"}, {"colmap", colmap}}));
797 return result.status();
798}
799
800// ─────────────────────────────────────────────────────────────────────────────
801// Save State Commands
802// ─────────────────────────────────────────────────────────────────────────────
803
804absl::Status MesenSocketClient::SaveState(int slot) {
805 auto result = SendCommand(
806 BuildJsonCommand("SAVESTATE", {{"slot", std::to_string(slot)}}));
807 return result.status();
808}
809
810absl::Status MesenSocketClient::LoadState(int slot) {
811 auto result = SendCommand(
812 BuildJsonCommand("LOADSTATE", {{"slot", std::to_string(slot)}}));
813 return result.status();
814}
815
816absl::StatusOr<std::string> MesenSocketClient::Screenshot() {
817 auto result = SendCommand(BuildJsonCommand("SCREENSHOT"));
818 if (!result.ok())
819 return result.status();
820 return *result; // Base64 PNG data
821}
822
823// ─────────────────────────────────────────────────────────────────────────────
824// Event Subscription
825// ─────────────────────────────────────────────────────────────────────────────
826
828 const std::vector<std::string>& events) {
829 if (!IsConnected()) {
830 return absl::FailedPreconditionError("Not connected to Mesen2");
831 }
832 if (events.empty()) {
833 return absl::InvalidArgumentError("At least one event must be requested");
834 }
835
836 // Ensure we don't leak a previous subscription socket/thread.
837 (void)Unsubscribe();
838
839 std::stringstream ss;
840 for (size_t i = 0; i < events.size(); ++i) {
841 if (i > 0)
842 ss << ",";
843 ss << events[i];
844 }
845
846 int event_fd = -1;
847 auto connect_status = ConnectSocketToPath(socket_path_, &event_fd);
848 if (!connect_status.ok()) {
849 return connect_status;
850 }
851
852 const std::string subscribe_command =
853 BuildJsonCommand("SUBSCRIBE", {{"events", ss.str()}});
854 const ssize_t sent =
855 send(event_fd, subscribe_command.c_str(), subscribe_command.length(), 0);
856 if (sent < 0) {
857 close(event_fd);
858 return absl::InternalError(
859 absl::StrCat("Failed to send command: ", strerror(errno)));
860 }
861
862 struct timeval tv;
863 tv.tv_sec = 5;
864 tv.tv_usec = 0;
865 setsockopt(event_fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&tv),
866 sizeof(tv));
867
868 std::string response;
869 char buffer[4096];
870 while (true) {
871 const ssize_t received = recv(event_fd, buffer, sizeof(buffer), 0);
872 if (received < 0) {
873#ifdef _WIN32
874 int err = WSAGetLastError();
875 if (err == WSAEWOULDBLOCK || err == WSAETIMEDOUT) {
876#else
877 if (errno == EAGAIN || errno == EWOULDBLOCK) {
878#endif
879 close(event_fd);
880 return absl::DeadlineExceededError(
881 "Timeout waiting for subscribe response");
882 }
883 close(event_fd);
884 return absl::InternalError(
885 absl::StrCat("Failed to receive response: ", strerror(errno)));
886 }
887 if (received == 0) {
888 break;
889 }
890 response.append(buffer, static_cast<size_t>(received));
891 if (response.size() > kMaxResponseSize) {
892 close(event_fd);
893 return absl::ResourceExhaustedError("Mesen2 response too large");
894 }
895 if (response.find('\n') != std::string::npos) {
896 break;
897 }
898 }
899
900 const size_t newline_pos = response.find('\n');
901 if (newline_pos == std::string::npos) {
902 close(event_fd);
903 return absl::DeadlineExceededError("Malformed subscribe response");
904 }
905
906 auto parse_status = ParseResponse(response.substr(0, newline_pos));
907 if (!parse_status.ok()) {
908 close(event_fd);
909 return parse_status.status();
910 }
911
912 pending_event_payload_ = response.substr(newline_pos + 1);
913 event_socket_fd_ = event_fd;
915 event_thread_ = std::thread(&MesenSocketClient::EventLoop, this);
916 return absl::OkStatus();
917}
918
920 event_thread_running_ = false;
921 ShutdownSocketFd(event_socket_fd_);
922 if (event_thread_.joinable()) {
923 event_thread_.join();
924 }
925
926 if (event_socket_fd_ >= 0) {
927 close(event_socket_fd_);
928 event_socket_fd_ = -1;
929 }
931
932 return absl::OkStatus();
933}
934
936 std::lock_guard<std::mutex> lock(event_callback_mutex_);
937 event_callback_ = std::move(callback);
938}
939
941 if (!callback) {
942 return 0;
943 }
944 std::lock_guard<std::mutex> lock(event_callback_mutex_);
946 event_listeners_[id] = std::move(callback);
947 return id;
948}
949
951 if (id == 0) {
952 return;
953 }
954 std::lock_guard<std::mutex> lock(event_callback_mutex_);
955 event_listeners_.erase(id);
956}
957
959 const int event_fd = event_socket_fd_;
960 if (event_fd < 0) {
961 return;
962 }
963
964 std::string pending = pending_event_payload_;
966 char buffer[4096];
967
968 auto dispatch_pending_lines = [&]() {
969 size_t newline_pos = pending.find('\n');
970 while (newline_pos != std::string::npos) {
971 std::string line = pending.substr(0, newline_pos);
972 pending.erase(0, newline_pos + 1);
973
974 if (!line.empty() && line.find("\"success\"") == std::string::npos) {
975 MesenEvent event;
976 event.raw_json = line;
977 event.type = ExtractJsonString(line, "event");
978 if (event.type.empty()) {
979 event.type = ExtractJsonString(line, "type");
980 }
981 event.address = static_cast<uint32_t>(
982 ExtractJsonInt(line, "address", ExtractJsonInt(line, "addr", 0)));
983 event.frame = static_cast<uint64_t>(ExtractJsonInt(line, "frame", 0));
984
985 EventCallback callback;
986 std::vector<EventCallback> listeners;
987 {
988 std::lock_guard<std::mutex> lock(event_callback_mutex_);
989 callback = event_callback_;
990 listeners.reserve(event_listeners_.size());
991 for (const auto& [id, listener] : event_listeners_) {
992 if (listener) {
993 listeners.push_back(listener);
994 }
995 }
996 }
997 if (callback && !event.type.empty()) {
998 callback(event);
999 }
1000 if (!event.type.empty()) {
1001 for (const auto& listener : listeners) {
1002 listener(event);
1003 }
1004 }
1005 }
1006
1007 newline_pos = pending.find('\n');
1008 }
1009 };
1010
1011 while (event_thread_running_) {
1012 dispatch_pending_lines();
1013
1014 const ssize_t received = recv(event_fd, buffer, sizeof(buffer), 0);
1015 if (received < 0) {
1016#ifdef _WIN32
1017 const int err = WSAGetLastError();
1018 if (err == WSAEINTR || err == WSAEWOULDBLOCK) {
1019 continue;
1020 }
1021#else
1022 if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
1023 continue;
1024 }
1025#endif
1026 break;
1027 }
1028
1029 if (received == 0) {
1030 break;
1031 }
1032
1033 pending.append(buffer, static_cast<size_t>(received));
1034 dispatch_pending_lines();
1035 }
1036}
1037
1038} // namespace mesen
1039} // namespace emu
1040} // namespace yaze
absl::Status ClearBreakpoints()
Clear all breakpoints.
absl::Status WriteWord(uint32_t addr, uint16_t value)
Write a 16-bit word to memory.
absl::StatusOr< uint8_t > ReadByte(uint32_t addr)
Read a single byte from memory.
void RemoveEventListener(EventListenerId id)
Remove a previously added event listener.
absl::Status Step(int count=1, const std::string &mode="into")
Step N CPU instructions (default 1)
void EventLoop()
Event listening thread function.
void SetEventCallback(EventCallback callback)
Set callback for received events.
absl::Status Ping()
Ping Mesen2 to check connectivity.
absl::Status Resume()
Resume emulation.
absl::StatusOr< CpuState > GetCpuState()
Get CPU register state.
absl::StatusOr< std::string > Screenshot()
Take a screenshot.
absl::StatusOr< std::string > ParseResponse(const std::string &response)
Parse JSON response for success/error.
absl::Status LoadState(int slot)
Load state from slot.
std::unordered_map< EventListenerId, EventCallback > event_listeners_
absl::StatusOr< uint16_t > ReadWord(uint32_t addr)
Read a 16-bit word from memory.
absl::StatusOr< int > AddBreakpoint(uint32_t addr, BreakpointType type, const std::string &condition="")
Add a breakpoint.
emu::input::ControllerState current_input_
static std::vector< std::string > ListAvailableSockets()
List available Mesen2 sockets on the system.
absl::Status SetButtons(const emu::input::ControllerState &state)
Set all buttons at once.
absl::StatusOr< std::string > GetTrace(int count=20)
Get execution trace log.
absl::StatusOr< GameState > GetGameState()
Get comprehensive ALTTP game state.
absl::Status SetButton(emu::input::SnesButton button, bool pressed)
Set controller button state.
absl::StatusOr< MesenState > GetState()
Get current emulation state.
absl::Status Frame()
Run exactly one frame.
absl::Status SetCollisionOverlay(bool enable, const std::string &colmap="A")
Enable/disable collision overlay.
bool IsConnected() const
Check if connected to Mesen2.
absl::StatusOr< std::string > SendCommand(const std::string &json)
Send a raw JSON command and get raw response.
EventListenerId AddEventListener(EventCallback callback)
Add an event listener without replacing existing listeners.
absl::StatusOr< std::vector< SpriteInfo > > GetSprites(bool all=false)
Get active sprites.
absl::Status RemoveBreakpoint(int id)
Remove a breakpoint by ID.
absl::Status Unsubscribe()
Unsubscribe from events.
absl::StatusOr< std::string > SendCommandOnSocket(int fd, const std::string &json, bool update_connection_state)
Send a command using a specific socket descriptor.
absl::Status Subscribe(const std::vector< std::string > &events)
Subscribe to events.
absl::StatusOr< std::string > Disassemble(uint32_t addr, int count=10)
Disassemble instructions at address.
absl::Status Pause()
Pause emulation.
absl::Status Reset()
Reset the console.
absl::Status SaveState(int slot)
Save state to slot.
void Disconnect()
Disconnect from Mesen2.
static std::vector< std::string > FindSocketPaths()
Find available Mesen2 socket paths.
absl::Status WriteByte(uint32_t addr, uint8_t value)
Write a single byte to memory.
absl::StatusOr< std::vector< uint8_t > > ReadBlock(uint32_t addr, size_t len)
Read a block of bytes from memory.
absl::Status Connect()
Auto-discover and connect to first available Mesen2 socket.
absl::Status WriteBlock(uint32_t addr, const std::vector< uint8_t > &data)
Write a block of bytes to memory.
SnesButton
SNES controller button mapping (platform-agnostic)
absl::Status ConnectSocketToPath(const std::string &socket_path, int *socket_fd)
std::string ExtractJsonString(const std::string &json, const std::string &key)
int64_t ExtractJsonInt(const std::string &json, const std::string &key, int64_t default_value=0)
bool ExtractJsonBool(const std::string &json, const std::string &key, bool default_value=false)
double ExtractJsonDouble(const std::string &json, const std::string &key, double default_value=0.0)
std::function< void(const MesenEvent &)> EventCallback
BreakpointType
Breakpoint types.
Controller state (16-bit SNES controller format)
void SetButton(SnesButton button, bool pressed)
bool IsPressed(SnesButton button) const
CPU register state from Mesen2.
Complete ALTTP game state from GAMESTATE command.
Event from Mesen2 subscription.
Emulation state from Mesen2.
Sprite information from SPRITES command.