12#include <unordered_map>
14#include "absl/flags/declare.h"
15#include "absl/flags/flag.h"
16#include "absl/strings/numbers.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
19#include "absl/strings/str_join.h"
20#include "absl/time/clock.h"
21#include "absl/time/time.h"
23#include "nlohmann/json.hpp"
35 bool connected =
false;
40 int last_breakpoint_id = -1;
41 std::string last_action =
"init";
55 const std::shared_ptr<emu::mesen::MesenSocketClient>& client) {
57 session.connected = client && client->IsConnected();
58 if (!session.connected) {
61 if (
auto state_or = client->GetState(); state_or.ok()) {
62 session.running = state_or->running;
63 session.paused = state_or->paused;
64 session.frame = state_or->frame;
66 if (
auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
68 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
73 return std::max(1, timeout_ms);
77 return std::clamp(poll_ms, 1, 1000);
81 return state_path +
".meta.json";
85 const char* label_for_errors) {
87 if (!std::filesystem::exists(path, ec) || ec) {
88 return ::absl::NotFoundError(
89 ::absl::StrFormat(
"%s not found: %s", label_for_errors, path));
91 if (std::filesystem::is_directory(path, ec) || ec) {
92 return ::absl::FailedPreconditionError(
93 ::absl::StrFormat(
"%s is a directory: %s", label_for_errors, path));
95 return ::absl::OkStatus();
98::absl::StatusOr<nlohmann::json> LoadJsonFile(
const std::string& path) {
99 std::ifstream in(path);
101 return ::absl::NotFoundError(::absl::StrFormat(
"File not found: %s", path));
106 }
catch (
const std::exception& e) {
107 return ::absl::InvalidArgumentError(
108 ::absl::StrFormat(
"Invalid JSON at %s: %s", path, e.what()));
113::absl::Status
WriteJsonFile(
const std::string& path,
const nlohmann::json& j) {
114 std::ofstream out(path);
115 if (!out.is_open()) {
116 return ::absl::PermissionDeniedError(
117 ::absl::StrFormat(
"Failed to open output file: %s", path));
121 return ::absl::InternalError(
122 ::absl::StrFormat(
"Failed to write output file: %s", path));
124 return ::absl::OkStatus();
128 return parser.
GetString(
"scenario").value_or(
"");
146 const std::string& state_path,
const std::string& rom_path,
147 const std::string& meta_path,
const std::string& expected_scenario) {
149 if (!state_status.ok()) {
153 if (!rom_status.ok()) {
157 if (!meta_status.ok()) {
161 auto meta_or = LoadJsonFile(meta_path);
163 return meta_or.status();
165 const auto& meta = *meta_or;
175 return ::absl::InternalError(
"Failed to hash ROM file");
178 return ::absl::InternalError(
"Failed to hash state file");
187 const bool state_match =
190 const bool scenario_match =
199 if (!scenario_match) {
208 formatter.
AddField(
"status", result.
fresh ?
"pass" :
"fail");
230 const std::string& state_path,
const std::string& rom_path,
231 const std::string& scenario,
const std::string& generator_name) {
233 if (!state_status.ok()) {
237 if (!rom_status.ok()) {
243 if (rom_sha1.empty()) {
244 return ::absl::InternalError(
"Failed to hash ROM file");
246 if (state_sha1.empty()) {
247 return ::absl::InternalError(
"Failed to hash state file");
251 meta[
"schema"] =
"mesen_state_meta/v1";
252 meta[
"state_path"] = state_path;
253 meta[
"state_sha1"] = state_sha1;
254 meta[
"rom_path"] = rom_path;
255 meta[
"rom_sha1"] = rom_sha1;
256 meta[
"generated_at"] = absl::FormatTime(absl::Now(), absl::UTCTimeZone());
257 meta[
"generator"] = generator_name;
258 if (!scenario.empty()) {
259 meta[
"scenario"] = scenario;
264 if (client && client->IsConnected()) {
265 nlohmann::json runtime;
266 if (
auto state_or = client->GetState(); state_or.ok()) {
267 runtime[
"frame"] = state_or->frame;
268 runtime[
"running"] = state_or->running;
269 runtime[
"paused"] = state_or->paused;
271 if (
auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
273 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
274 runtime[
"pc"] = absl::StrFormat(
"0x%06X", pc);
276 if (
auto game_or = client->GetGameState(); game_or.ok()) {
277 runtime[
"indoors"] = game_or->game.indoors;
278 runtime[
"room_id"] = absl::StrFormat(
"0x%04X", game_or->game.room_id);
279 runtime[
"link_x"] = game_or->link.x;
280 runtime[
"link_y"] = game_or->link.y;
282 if (!runtime.empty()) {
283 meta[
"runtime"] = runtime;
290 const std::filesystem::path& states_dir) {
292 if (!std::filesystem::exists(states_dir, ec) || ec ||
293 !std::filesystem::is_directory(states_dir, ec) || ec) {
294 return ::absl::NotFoundError(::absl::StrFormat(
295 "states directory not found: %s", states_dir.string()));
299 std::filesystem::path latest_path;
300 std::filesystem::file_time_type latest_time;
301 for (
const auto& entry :
302 std::filesystem::directory_iterator(states_dir, ec)) {
306 if (!entry.is_regular_file()) {
309 const auto ext = entry.path().extension().string();
310 if (ext !=
".state" && ext !=
".mss") {
313 const auto file_time = entry.last_write_time(ec);
317 if (!found || file_time > latest_time) {
319 latest_time = file_time;
320 latest_path = entry.path();
324 return ::absl::NotFoundError(::absl::StrFormat(
325 "no state files (.state/.mss) found in %s", states_dir.string()));
333 j[
"connected"] = s.connected;
334 j[
"running"] = s.running;
335 j[
"paused"] = s.paused;
336 j[
"frame"] = s.frame;
338 j[
"last_breakpoint_id"] = s.last_breakpoint_id;
339 j[
"last_action"] = s.last_action;
340 j[
"breakpoints"] = nlohmann::json::array();
341 for (
const auto& [
id, addr] : s.breakpoints_by_id) {
342 j[
"breakpoints"].push_back({{
"id",
id}, {
"address", addr}});
345 std::ofstream out(file_path);
346 if (!out.is_open()) {
347 return ::absl::PermissionDeniedError(
"Failed to open session export path");
351 return ::absl::InternalError(
"Failed to write session export file");
353 return ::absl::OkStatus();
357 std::ifstream in(file_path);
359 return ::absl::NotFoundError(
"Session import file not found");
364 }
catch (
const std::exception& e) {
365 return ::absl::InvalidArgumentError(
366 ::absl::StrFormat(
"Invalid session JSON: %s", e.what()));
370 loaded.
connected = j.value(
"connected",
false);
371 loaded.
running = j.value(
"running",
false);
372 loaded.
paused = j.value(
"paused",
false);
373 loaded.
frame = j.value(
"frame", 0ULL);
374 loaded.
pc = j.value(
"pc", 0U);
376 loaded.
last_action = j.value(
"last_action",
"import");
377 if (j.contains(
"breakpoints") && j[
"breakpoints"].is_array()) {
378 for (
const auto& bp : j[
"breakpoints"]) {
379 const int id = bp.value(
"id", -1);
380 const uint32_t addr = bp.value(
"address", 0U);
388 return ::absl::OkStatus();
393 if (!client->IsConnected()) {
394 std::string socket_path = ::absl::GetFlag(FLAGS_mesen_socket);
395 if (socket_path.empty()) {
396 const char* env_path = std::getenv(
"MESEN2_SOCKET_PATH");
397 if (env_path && env_path[0] !=
'\0') {
398 socket_path = env_path;
402 socket_path.empty() ? client->Connect() : client->Connect(socket_path);
404 return ::absl::Status(
406 ::absl::StrCat(
"Not connected to Mesen2. Is Mesen2-OoS running? ",
410 session.connected =
true;
411 session.last_action =
"connect";
414 return ::absl::OkStatus();
418 const std::string&
name,
421 return default_value;
428 hex.reserve(data_str.size());
429 for (
char c : data_str) {
430 if (std::isxdigit(
static_cast<unsigned char>(c))) {
435 std::vector<uint8_t> data;
436 for (
size_t i = 0; i + 1 < hex.size(); i += 2) {
438 auto res = std::from_chars(hex.data() + i, hex.data() + i + 2,
byte, 16);
439 if (res.ec == std::errc()) {
440 data.push_back(
static_cast<uint8_t
>(
byte));
447 const std::shared_ptr<emu::mesen::MesenSocketClient>& client,
448 ::absl::Time deadline) {
449 while (::absl::Now() < deadline) {
450 auto cpu_or = client->GetCpuState();
454 std::this_thread::sleep_for(std::chrono::milliseconds(5));
456 return ::absl::DeadlineExceededError(
457 "Timed out while polling Mesen2 CPU state");
466 return ::absl::OkStatus();
474 auto status = EnsureConnected();
479 auto result = client->GetGameState();
481 return result.status();
483 const auto& state = *result;
484 formatter.
AddField(
"link_x", state.link.x);
485 formatter.
AddField(
"link_y", state.link.y);
486 formatter.
AddField(
"link_layer", state.link.layer);
487 formatter.
AddField(
"link_direction",
static_cast<int>(state.link.direction));
488 formatter.
AddField(
"health",
static_cast<int>(state.items.current_health));
489 formatter.
AddField(
"max_health",
static_cast<int>(state.items.max_health));
490 formatter.
AddField(
"magic",
static_cast<int>(state.items.magic));
491 formatter.
AddField(
"rupees",
static_cast<int>(state.items.rupees));
492 formatter.
AddField(
"bombs",
static_cast<int>(state.items.bombs));
493 formatter.
AddField(
"arrows",
static_cast<int>(state.items.arrows));
494 formatter.
AddField(
"mode",
static_cast<int>(state.game.mode));
495 formatter.
AddField(
"submode",
static_cast<int>(state.game.submode));
496 formatter.
AddField(
"indoors", state.game.indoors);
497 if (state.game.indoors) {
498 formatter.
AddHexField(
"room_id", state.game.room_id, 4);
500 formatter.
AddHexField(
"overworld_area", state.game.overworld_area, 2);
503 return ::absl::OkStatus();
510 return ::absl::OkStatus();
517 auto status = EnsureConnected();
521 bool show_all = parser.
HasFlag(
"all");
523 auto result = client->GetSprites(show_all);
525 return result.status();
527 formatter.
AddField(
"count",
static_cast<int>(result->size()));
529 for (
const auto& sprite : *result) {
531 "[#%d] type=0x%02X state=%d @(%d,%d) hp=%d", sprite.slot, sprite.type,
532 sprite.state, sprite.x, sprite.y, sprite.health));
536 return ::absl::OkStatus();
543 return ::absl::OkStatus();
551 auto status = EnsureConnected();
556 auto result = client->GetCpuState();
558 return result.status();
560 const auto& cpu = *result;
561 uint32_t pc = (
static_cast<uint32_t
>(cpu.K) << 16) | (cpu.PC & 0xFFFF);
570 formatter.
AddField(
"emulation_mode", cpu.emulation_mode);
572 return ::absl::OkStatus();
585 auto status = EnsureConnected();
589 auto addr_or = parser.
GetHex(
"address");
591 return addr_or.status();
592 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
594 auto length_or = ParseOptionalInt(parser,
"length", 16);
596 return length_or.status();
597 int length = *length_or;
600 auto result = client->ReadBlock(addr, length);
602 return result.status();
605 formatter.
AddField(
"length",
static_cast<int>(result->size()));
607 for (uint8_t
byte : *result) {
608 formatter.
AddArrayItem(::absl::StrFormat(
"%02X",
byte));
612 return ::absl::OkStatus();
625 auto status = EnsureConnected();
629 auto addr_or = parser.
GetHex(
"address");
631 return addr_or.status();
632 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
634 auto data_str = parser.
GetString(
"data");
635 if (!data_str.has_value()) {
636 return ::absl::InvalidArgumentError(
"--data is required");
639 auto data = ParseHexBytes(*data_str);
641 return ::absl::InvalidArgumentError(
"Invalid --data hex string");
645 auto write_status = client->WriteBlock(addr, data);
646 if (!write_status.ok())
650 formatter.
AddField(
"bytes_written",
static_cast<int>(data.size()));
652 return ::absl::OkStatus();
665 auto status = EnsureConnected();
669 auto addr_or = parser.
GetHex(
"address");
671 return addr_or.status();
672 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
674 auto count_or = ParseOptionalInt(parser,
"count", 10);
676 return count_or.status();
677 int count = *count_or;
680 auto result = client->Disassemble(addr, count);
682 return result.status();
685 formatter.
AddField(
"disassembly", *result);
686 return ::absl::OkStatus();
693 return ::absl::OkStatus();
700 auto status = EnsureConnected();
704 auto count_or = ParseOptionalInt(parser,
"count", 20);
706 return count_or.status();
707 int count = *count_or;
710 auto result = client->GetTrace(count);
712 return result.status();
715 formatter.
AddField(
"trace", *result);
716 return ::absl::OkStatus();
729 auto status = EnsureConnected();
733 auto action = parser.
GetString(
"action");
734 if (!action.has_value()) {
735 return ::absl::InvalidArgumentError(
"--action is required");
738 if (*action ==
"add") {
739 auto addr_or = parser.
GetHex(
"address");
741 return addr_or.status();
742 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
744 std::string type_str = parser.
GetString(
"type").value_or(
"exec");
746 if (type_str ==
"read")
748 else if (type_str ==
"write")
750 else if (type_str ==
"rw")
754 auto result = client->AddBreakpoint(addr, type);
756 return result.status();
757 formatter.
AddField(
"breakpoint_id", *result);
759 formatter.
AddField(
"status",
"added");
760 }
else if (*action ==
"remove") {
761 auto id_or = parser.
GetInt(
"id");
763 return id_or.status();
767 auto remove_status = client->RemoveBreakpoint(
id);
768 if (!remove_status.ok())
769 return remove_status;
770 formatter.
AddField(
"breakpoint_id",
id);
771 formatter.
AddField(
"status",
"removed");
772 }
else if (*action ==
"clear") {
774 auto clear_status = client->ClearBreakpoints();
775 if (!clear_status.ok())
777 formatter.
AddField(
"status",
"cleared");
778 }
else if (*action ==
"list") {
779 formatter.
AddField(
"status",
"not_implemented");
781 return ::absl::InvalidArgumentError(
"Unknown action: " + *action);
784 return ::absl::OkStatus();
797 auto status = EnsureConnected();
801 auto action = parser.
GetString(
"action");
802 if (!action.has_value()) {
803 return ::absl::InvalidArgumentError(
"--action required");
807 if (*action ==
"pause") {
808 auto result = client->Pause();
811 }
else if (*action ==
"resume") {
812 auto result = client->Resume();
815 }
else if (*action ==
"step") {
816 auto result = client->Step(1);
819 }
else if (*action ==
"frame") {
820 auto result = client->Frame();
823 }
else if (*action ==
"reset") {
824 auto result = client->Reset();
828 return ::absl::InvalidArgumentError(
"Unknown action: " + *action);
831 auto& session = SessionState();
832 session.last_action = *action;
833 UpdateSessionFromRuntime(client);
835 formatter.
AddField(
"action", *action);
836 return ::absl::OkStatus();
842 if (!parser.
GetString(
"action").has_value()) {
843 return ::absl::OkStatus();
845 const auto action = parser.
GetString(
"action").value_or(
"show");
846 if (action ==
"show" || action ==
"reset") {
847 return ::absl::OkStatus();
849 if (action ==
"export" || action ==
"import") {
852 return ::absl::InvalidArgumentError(
853 "--action must be show|reset|export|import");
857 const auto& session = SessionState();
858 formatter.
AddField(
"connected", session.connected);
859 formatter.
AddField(
"running", session.running);
860 formatter.
AddField(
"paused", session.paused);
861 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
863 formatter.
AddField(
"breakpoint_count",
864 static_cast<int>(session.breakpoints_by_id.size()));
865 formatter.
AddField(
"last_breakpoint_id", session.last_breakpoint_id);
866 formatter.
AddField(
"last_action", session.last_action);
867 return ::absl::OkStatus();
874 const auto action = parser.
GetString(
"action").value_or(
"show");
875 if (action ==
"reset") {
877 auto& session = SessionState();
878 session.last_action =
"reset";
880 formatter.
AddField(
"action",
"reset");
884 if (action ==
"export") {
886 if (!file.has_value()) {
887 return ::absl::InvalidArgumentError(
"--file required for export");
889 auto export_status = ExportSessionStateToFile(*file);
890 if (!export_status.ok()) {
891 return export_status;
893 auto& session = SessionState();
894 session.last_action =
"export";
896 formatter.
AddField(
"action",
"export");
901 if (action ==
"import") {
903 if (!file.has_value()) {
904 return ::absl::InvalidArgumentError(
"--file required for import");
906 auto import_status = ImportSessionStateFromFile(*file);
907 if (!import_status.ok()) {
908 return import_status;
910 auto& session = SessionState();
911 session.last_action =
"import";
913 formatter.
AddField(
"action",
"import");
918 auto status = EnsureConnected();
923 UpdateSessionFromRuntime(client);
934 const auto type = parser.
GetString(
"type").value_or(
"");
935 if (type ==
"frame") {
941 if (type ==
"breakpoint") {
944 return ::absl::InvalidArgumentError(
945 "--type must be one of frame|pc|breakpoint");
952 auto status = EnsureConnected();
957 auto& session = SessionState();
959 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
960 if (!timeout_or.ok()) {
961 return timeout_or.status();
963 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
965 return poll_or.status();
967 const int timeout_ms = ClampTimeoutMs(*timeout_or);
968 const int poll_ms = ClampPollMs(*poll_or);
969 const auto start = ::absl::Now();
970 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
972 const auto type = parser.
GetString(
"type").value_or(
"");
973 if (type ==
"frame") {
974 auto count_or = parser.
GetInt(
"count");
975 if (!count_or.ok()) {
976 return count_or.status();
978 UpdateSessionFromRuntime(client);
979 const uint64_t target =
980 session.frame +
static_cast<uint64_t
>(std::max(0, *count_or));
981 while (::absl::Now() < deadline) {
982 if (
auto state_or = client->GetState(); state_or.ok()) {
983 session.frame = state_or->frame;
984 session.running = state_or->running;
985 session.paused = state_or->paused;
986 if (session.frame >= target) {
987 session.last_action =
"await-frame";
989 formatter.
AddField(
"type",
"frame");
990 formatter.
AddField(
"target_frame",
static_cast<uint64_t
>(target));
991 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
993 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
994 ::absl::Now() - start)));
995 return ::absl::OkStatus();
998 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1000 return ::absl::DeadlineExceededError(
"Timed out waiting for target frame");
1004 auto addr_or = parser.
GetHex(
"address");
1005 if (!addr_or.ok()) {
1006 return addr_or.status();
1008 const uint32_t target_pc =
static_cast<uint32_t
>(*addr_or);
1009 while (::absl::Now() < deadline) {
1010 auto cpu_or = PollCpuStateWithDeadline(client, deadline);
1012 return cpu_or.status();
1015 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1017 if (pc == target_pc) {
1018 session.last_action =
"await-pc";
1019 formatter.
AddField(
"status",
"ok");
1024 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
1025 ::absl::Now() - start)));
1026 return ::absl::OkStatus();
1028 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1030 return ::absl::DeadlineExceededError(
"Timed out waiting for target pc");
1033 auto id_or = parser.
GetInt(
"id");
1035 return id_or.status();
1037 const int id = *id_or;
1038 const auto it = session.breakpoints_by_id.find(
id);
1039 if (it == session.breakpoints_by_id.end()) {
1040 return ::absl::NotFoundError(
"Unknown breakpoint id in session state");
1042 const uint32_t target_pc = it->second;
1043 while (::absl::Now() < deadline) {
1044 auto state_or = client->GetState();
1045 if (!state_or.ok()) {
1046 return state_or.status();
1048 session.running = state_or->running;
1049 session.paused = state_or->paused;
1050 session.frame = state_or->frame;
1051 auto cpu_or = client->GetCpuState();
1053 return cpu_or.status();
1056 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1058 if (session.paused && pc == target_pc) {
1059 session.last_action =
"await-breakpoint";
1060 formatter.
AddField(
"status",
"ok");
1061 formatter.
AddField(
"type",
"breakpoint");
1062 formatter.
AddField(
"breakpoint_id",
id);
1064 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1066 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
1067 ::absl::Now() - start)));
1068 return ::absl::OkStatus();
1070 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1072 return ::absl::DeadlineExceededError(
"Timed out waiting for breakpoint hit");
1082 const auto goal = parser.
GetString(
"goal").value_or(
"");
1083 if (goal ==
"break-at" || goal ==
"capture-state-at-pc") {
1086 if (goal ==
"run-frames") {
1089 return ::absl::InvalidArgumentError(
1090 "Unknown --goal. Supported: break-at|run-frames|capture-state-at-pc");
1097 auto status = EnsureConnected();
1102 auto& session = SessionState();
1103 const auto goal = parser.
GetString(
"goal").value_or(
"");
1104 if (goal ==
"run-frames") {
1105 auto count_or = parser.
GetInt(
"count");
1106 if (!count_or.ok()) {
1107 return count_or.status();
1109 const int frame_count = std::max(1, *count_or);
1110 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
1111 if (!timeout_or.ok()) {
1112 return timeout_or.status();
1114 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
1115 if (!poll_or.ok()) {
1116 return poll_or.status();
1118 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1119 const int poll_ms = ClampPollMs(*poll_or);
1120 const auto start = ::absl::Now();
1121 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1123 if (
auto pause_status = client->Pause(); !pause_status.ok()) {
1124 return pause_status;
1126 UpdateSessionFromRuntime(client);
1127 const uint64_t target_frame =
1128 session.frame +
static_cast<uint64_t
>(frame_count);
1129 if (
auto resume_status = client->Resume(); !resume_status.ok()) {
1130 return resume_status;
1132 bool reached =
false;
1133 while (::absl::Now() < deadline) {
1134 auto state_or = client->GetState();
1135 if (!state_or.ok()) {
1138 session.running = state_or->running;
1139 session.paused = state_or->paused;
1140 session.frame = state_or->frame;
1141 if (session.frame >= target_frame) {
1145 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1147 (void)client->Pause();
1148 UpdateSessionFromRuntime(client);
1150 return ::absl::DeadlineExceededError(
"Goal run-frames timed out");
1152 session.last_action =
"goal-run-frames";
1153 formatter.
AddField(
"status",
"ok");
1154 formatter.
AddField(
"goal",
"run-frames");
1155 formatter.
AddField(
"requested_frames", frame_count);
1156 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1158 static_cast<uint64_t
>(
1159 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1160 return ::absl::OkStatus();
1163 if (goal !=
"break-at" && goal !=
"capture-state-at-pc") {
1164 return ::absl::InvalidArgumentError(
"Unsupported goal");
1166 auto addr_or = parser.
GetHex(
"address");
1167 if (!addr_or.ok()) {
1168 return addr_or.status();
1170 const uint32_t target_pc =
static_cast<uint32_t
>(*addr_or);
1171 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
1172 if (!timeout_or.ok()) {
1173 return timeout_or.status();
1175 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
1176 if (!poll_or.ok()) {
1177 return poll_or.status();
1179 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1180 const int poll_ms = ClampPollMs(*poll_or);
1181 const auto start = ::absl::Now();
1182 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1184 if (
auto pause_status = client->Pause(); !pause_status.ok()) {
1185 return pause_status;
1190 return bp_or.status();
1192 const int bp_id = *bp_or;
1193 session.breakpoints_by_id[bp_id] = target_pc;
1194 session.last_breakpoint_id = bp_id;
1196 if (
auto resume_status = client->Resume(); !resume_status.ok()) {
1197 (void)client->RemoveBreakpoint(bp_id);
1198 session.breakpoints_by_id.erase(bp_id);
1199 return resume_status;
1203 while (::absl::Now() < deadline) {
1204 auto state_or = client->GetState();
1205 if (!state_or.ok()) {
1208 session.running = state_or->running;
1209 session.paused = state_or->paused;
1210 session.frame = state_or->frame;
1211 auto cpu_or = client->GetCpuState();
1216 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1218 if (session.paused && pc == target_pc) {
1222 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1225 (void)client->Pause();
1226 (void)client->RemoveBreakpoint(bp_id);
1227 session.breakpoints_by_id.erase(bp_id);
1228 UpdateSessionFromRuntime(client);
1230 return ::absl::DeadlineExceededError(
"Goal break-at timed out");
1233 session.last_action = (goal ==
"capture-state-at-pc")
1234 ?
"goal-capture-state-at-pc"
1236 if (goal ==
"capture-state-at-pc") {
1237 auto game_state_or = client->GetGameState();
1238 if (game_state_or.ok()) {
1239 formatter.
AddHexField(
"room_id", game_state_or->game.room_id, 4);
1240 formatter.
AddField(
"indoors", game_state_or->game.indoors);
1241 formatter.
AddField(
"link_x", game_state_or->link.x);
1242 formatter.
AddField(
"link_y", game_state_or->link.y);
1243 formatter.
AddField(
"health", game_state_or->items.current_health);
1246 formatter.
AddField(
"status",
"ok");
1248 formatter.
AddField(
"breakpoint_id", bp_id);
1251 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1253 static_cast<uint64_t
>(
1254 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1255 return ::absl::OkStatus();
1268 const std::string state_path = *parser.
GetString(
"state");
1269 const std::string rom_path = *parser.
GetString(
"rom-file");
1270 const std::string meta_path =
1271 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1272 const std::string expected_scenario = OptionalScenario(parser);
1274 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1276 if (!result_or.ok()) {
1277 return result_or.status();
1279 AddSavestateFreshnessFields(formatter, *result_or);
1280 if (!result_or->fresh) {
1281 return ::absl::FailedPreconditionError(
1282 "Savestate freshness verification failed");
1284 return ::absl::OkStatus();
1297 const std::string state_path = *parser.
GetString(
"state");
1298 const std::string rom_path = *parser.
GetString(
"rom-file");
1299 const std::string meta_path =
1300 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1301 const std::string scenario = OptionalScenario(parser);
1303 auto meta_or = BuildSavestateMetadata(state_path, rom_path, scenario,
1304 "z3ed mesen-state-regen");
1305 if (!meta_or.ok()) {
1306 return meta_or.status();
1309 auto write_status = WriteJsonFile(meta_path, *meta_or);
1310 if (!write_status.ok())
1311 return write_status;
1313 formatter.
AddField(
"status",
"ok");
1314 formatter.
AddField(
"state", state_path);
1315 formatter.
AddField(
"rom", rom_path);
1316 formatter.
AddField(
"meta", meta_path);
1317 formatter.
AddField(
"rom_sha1", meta_or->value(
"rom_sha1",
""));
1318 formatter.
AddField(
"state_sha1", meta_or->value(
"state_sha1",
""));
1319 formatter.
AddField(
"scenario", scenario);
1320 return ::absl::OkStatus();
1333 const std::string requested_state_path = *parser.
GetString(
"state");
1334 const std::string rom_path = *parser.
GetString(
"rom-file");
1335 const std::string scenario = OptionalScenario(parser);
1336 const std::string meta_path = parser.
GetString(
"meta").value_or(
1337 DefaultMetaPathForState(requested_state_path));
1338 const int wait_ms = parser.
GetInt(
"wait-ms").value_or(400);
1339 const int slot = parser.
GetInt(
"slot").value_or(-1);
1340 const std::string states_dir = parser.
GetString(
"states-dir").value_or(
"");
1342 auto rom_status = EnsureFileExists(rom_path,
"ROM file");
1343 if (!rom_status.ok()) {
1347 std::string resolved_state_path = requested_state_path;
1348 bool captured_from_mesen_slot =
false;
1350 auto status = EnsureConnected();
1355 auto save_status = client->SaveState(slot);
1356 if (!save_status.ok()) {
1359 captured_from_mesen_slot =
true;
1360 std::this_thread::sleep_for(
1361 std::chrono::milliseconds(std::max(1, wait_ms)));
1365 if (!std::filesystem::exists(resolved_state_path, ec) || ec) {
1366 if (states_dir.empty()) {
1367 return ::absl::NotFoundError(
1368 ::absl::StrFormat(
"state file not found after capture: %s (use "
1369 "--states-dir to auto-locate latest state file)",
1370 resolved_state_path));
1372 auto latest_or = FindLatestStateFileInDir(states_dir);
1373 if (!latest_or.ok()) {
1374 return latest_or.status();
1376 const auto source_path = *latest_or;
1377 auto parent = std::filesystem::path(resolved_state_path).parent_path();
1378 if (!parent.empty()) {
1379 std::filesystem::create_directories(parent, ec);
1381 return ::absl::PermissionDeniedError(::absl::StrFormat(
1382 "failed to create directory: %s", parent.string()));
1385 std::filesystem::copy_file(
1386 source_path, resolved_state_path,
1387 std::filesystem::copy_options::overwrite_existing, ec);
1389 return ::absl::InternalError(::absl::StrFormat(
1390 "failed to copy state from %s to %s: %s", source_path.string(),
1391 resolved_state_path, ec.message()));
1395 auto meta_or = BuildSavestateMetadata(resolved_state_path, rom_path, scenario,
1396 "z3ed mesen-state-capture");
1397 if (!meta_or.ok()) {
1398 return meta_or.status();
1400 auto write_status = WriteJsonFile(meta_path, *meta_or);
1401 if (!write_status.ok()) {
1402 return write_status;
1405 auto verify_or = ComputeSavestateFreshness(resolved_state_path, rom_path,
1406 meta_path, scenario);
1407 if (!verify_or.ok()) {
1408 return verify_or.status();
1410 formatter.
AddField(
"status",
"ok");
1411 formatter.
AddField(
"state", resolved_state_path);
1412 formatter.
AddField(
"rom", rom_path);
1413 formatter.
AddField(
"meta", meta_path);
1414 formatter.
AddField(
"scenario", scenario);
1415 formatter.
AddField(
"captured_from_mesen_slot", captured_from_mesen_slot);
1417 formatter.
AddField(
"fresh", verify_or->fresh);
1418 formatter.
AddField(
"rom_sha1", meta_or->value(
"rom_sha1",
""));
1419 formatter.
AddField(
"state_sha1", meta_or->value(
"state_sha1",
""));
1420 return ::absl::OkStatus();
1433 const std::string state_path = *parser.
GetString(
"state");
1434 const std::string rom_path = *parser.
GetString(
"rom-file");
1435 const std::string meta_path =
1436 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1437 const std::string expected_scenario = OptionalScenario(parser);
1439 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1441 if (!result_or.ok()) {
1442 return result_or.status();
1445 AddSavestateFreshnessFields(formatter, *result_or);
1449 "fresh=%s scenario=%s rom_sha1=%s state_sha1=%s",
1450 result_or->fresh ?
"true" :
"false",
1451 result_or->recorded_scenario.empty() ?
"(none)"
1452 : result_or->recorded_scenario,
1453 result_or->current_rom_sha1, result_or->current_state_sha1));
1454 if (!result_or->fresh) {
1455 return ::absl::FailedPreconditionError(
"mesen preflight hook failed");
1457 return ::absl::OkStatus();
1461std::vector<std::unique_ptr<resources::CommandHandler>>
1463 std::vector<std::unique_ptr<resources::CommandHandler>> handlers;
1464 handlers.push_back(std::make_unique<MesenGamestateCommandHandler>());
1465 handlers.push_back(std::make_unique<MesenSpritesCommandHandler>());
1466 handlers.push_back(std::make_unique<MesenCpuCommandHandler>());
1467 handlers.push_back(std::make_unique<MesenMemoryReadCommandHandler>());
1468 handlers.push_back(std::make_unique<MesenMemoryWriteCommandHandler>());
1469 handlers.push_back(std::make_unique<MesenDisasmCommandHandler>());
1470 handlers.push_back(std::make_unique<MesenTraceCommandHandler>());
1471 handlers.push_back(std::make_unique<MesenBreakpointCommandHandler>());
1472 handlers.push_back(std::make_unique<MesenControlCommandHandler>());
1473 handlers.push_back(std::make_unique<MesenSessionCommandHandler>());
1474 handlers.push_back(std::make_unique<MesenAwaitCommandHandler>());
1475 handlers.push_back(std::make_unique<MesenGoalCommandHandler>());
1476 handlers.push_back(std::make_unique<MesenStateVerifyCommandHandler>());
1477 handlers.push_back(std::make_unique<MesenStateRegenCommandHandler>());
1478 handlers.push_back(std::make_unique<MesenStateCaptureCommandHandler>());
1479 handlers.push_back(std::make_unique<MesenStateHookCommandHandler>());
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
absl::StatusOr< int > GetHex(const std::string &name) const
Parse a hex integer argument.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
static std::shared_ptr< MesenSocketClient > GetOrCreate()
ABSL_DECLARE_FLAG(std::string, mesen_socket)
::absl::StatusOr< nlohmann::json > BuildSavestateMetadata(const std::string &state_path, const std::string &rom_path, const std::string &scenario, const std::string &generator_name)
::absl::Status EnsureConnected()
::absl::Status WriteJsonFile(const std::string &path, const nlohmann::json &j)
::absl::StatusOr< int > ParseOptionalInt(const resources::ArgumentParser &parser, const std::string &name, int default_value)
void AddSavestateFreshnessFields(resources::OutputFormatter &formatter, const SavestateFreshnessResult &result)
::absl::StatusOr< std::filesystem::path > FindLatestStateFileInDir(const std::filesystem::path &states_dir)
::absl::Status EnsureFileExists(const std::string &path, const char *label_for_errors)
std::vector< uint8_t > ParseHexBytes(const std::string &data_str)
::absl::Status ExportSessionStateToFile(const std::string &file_path)
EmulatorAgentSessionState & SessionState()
::absl::StatusOr< SavestateFreshnessResult > ComputeSavestateFreshness(const std::string &state_path, const std::string &rom_path, const std::string &meta_path, const std::string &expected_scenario)
::absl::StatusOr< emu::mesen::CpuState > PollCpuStateWithDeadline(const std::shared_ptr< emu::mesen::MesenSocketClient > &client, ::absl::Time deadline)
::absl::Status ImportSessionStateFromFile(const std::string &file_path)
std::string DefaultMetaPathForState(const std::string &state_path)
void UpdateSessionFromRuntime(const std::shared_ptr< emu::mesen::MesenSocketClient > &client)
int ClampTimeoutMs(int timeout_ms)
std::string OptionalScenario(const resources::ArgumentParser &parser)
int ClampPollMs(int poll_ms)
std::vector< std::unique_ptr< resources::CommandHandler > > CreateMesenCommandHandlers()
Factory function to create Mesen2 command handlers.
::absl::Status AddSessionFields(resources::OutputFormatter &formatter)
BreakpointType
Breakpoint types.
std::string ComputeFileSha1Hex(const std::string &path)
std::unordered_map< int, uint32_t > breakpoints_by_id
std::string current_rom_sha1
std::string current_state_sha1
std::string recorded_state_sha1
std::vector< std::string > stale_reasons
std::string recorded_scenario
std::string expected_scenario
std::string recorded_rom_sha1