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_format.h"
18#include "absl/strings/str_join.h"
19#include "absl/time/clock.h"
20#include "absl/time/time.h"
22#include "nlohmann/json.hpp"
34 bool connected =
false;
39 int last_breakpoint_id = -1;
40 std::string last_action =
"init";
54 const std::shared_ptr<emu::mesen::MesenSocketClient>& client) {
56 session.connected = client && client->IsConnected();
57 if (!session.connected) {
60 if (
auto state_or = client->GetState(); state_or.ok()) {
61 session.running = state_or->running;
62 session.paused = state_or->paused;
63 session.frame = state_or->frame;
65 if (
auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
67 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
72 return std::max(1, timeout_ms);
76 return std::clamp(poll_ms, 1, 1000);
80 return state_path +
".meta.json";
84 const char* label_for_errors) {
86 if (!std::filesystem::exists(path, ec) || ec) {
87 return ::absl::NotFoundError(
88 ::absl::StrFormat(
"%s not found: %s", label_for_errors, path));
90 if (std::filesystem::is_directory(path, ec) || ec) {
91 return ::absl::FailedPreconditionError(
92 ::absl::StrFormat(
"%s is a directory: %s", label_for_errors, path));
94 return ::absl::OkStatus();
97::absl::StatusOr<nlohmann::json>
LoadJsonFile(
const std::string& path) {
98 std::ifstream in(path);
100 return ::absl::NotFoundError(::absl::StrFormat(
"File not found: %s", path));
105 }
catch (
const std::exception& e) {
106 return ::absl::InvalidArgumentError(
107 ::absl::StrFormat(
"Invalid JSON at %s: %s", path, e.what()));
112::absl::Status
WriteJsonFile(
const std::string& path,
const nlohmann::json& j) {
113 std::ofstream out(path);
114 if (!out.is_open()) {
115 return ::absl::PermissionDeniedError(
116 ::absl::StrFormat(
"Failed to open output file: %s", path));
120 return ::absl::InternalError(
121 ::absl::StrFormat(
"Failed to write output file: %s", path));
123 return ::absl::OkStatus();
127 return parser.
GetString(
"scenario").value_or(
"");
145 const std::string& state_path,
const std::string& rom_path,
146 const std::string& meta_path,
const std::string& expected_scenario) {
148 if (!state_status.ok()) {
152 if (!rom_status.ok()) {
156 if (!meta_status.ok()) {
162 return meta_or.status();
164 const auto& meta = *meta_or;
174 return ::absl::InternalError(
"Failed to hash ROM file");
177 return ::absl::InternalError(
"Failed to hash state file");
186 const bool state_match =
189 const bool scenario_match =
198 if (!scenario_match) {
207 formatter.
AddField(
"status", result.
fresh ?
"pass" :
"fail");
229 const std::string& state_path,
const std::string& rom_path,
230 const std::string& scenario,
const std::string& generator_name) {
232 if (!state_status.ok()) {
236 if (!rom_status.ok()) {
242 if (rom_sha1.empty()) {
243 return ::absl::InternalError(
"Failed to hash ROM file");
245 if (state_sha1.empty()) {
246 return ::absl::InternalError(
"Failed to hash state file");
250 meta[
"schema"] =
"mesen_state_meta/v1";
251 meta[
"state_path"] = state_path;
252 meta[
"state_sha1"] = state_sha1;
253 meta[
"rom_path"] = rom_path;
254 meta[
"rom_sha1"] = rom_sha1;
255 meta[
"generated_at"] = absl::FormatTime(absl::Now(), absl::UTCTimeZone());
256 meta[
"generator"] = generator_name;
257 if (!scenario.empty()) {
258 meta[
"scenario"] = scenario;
263 if (client && client->IsConnected()) {
264 nlohmann::json runtime;
265 if (
auto state_or = client->GetState(); state_or.ok()) {
266 runtime[
"frame"] = state_or->frame;
267 runtime[
"running"] = state_or->running;
268 runtime[
"paused"] = state_or->paused;
270 if (
auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
272 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
273 runtime[
"pc"] = absl::StrFormat(
"0x%06X", pc);
275 if (
auto game_or = client->GetGameState(); game_or.ok()) {
276 runtime[
"indoors"] = game_or->game.indoors;
277 runtime[
"room_id"] = absl::StrFormat(
"0x%04X", game_or->game.room_id);
278 runtime[
"link_x"] = game_or->link.x;
279 runtime[
"link_y"] = game_or->link.y;
281 if (!runtime.empty()) {
282 meta[
"runtime"] = runtime;
289 const std::filesystem::path& states_dir) {
291 if (!std::filesystem::exists(states_dir, ec) || ec ||
292 !std::filesystem::is_directory(states_dir, ec) || ec) {
293 return ::absl::NotFoundError(::absl::StrFormat(
294 "states directory not found: %s", states_dir.string()));
298 std::filesystem::path latest_path;
299 std::filesystem::file_time_type latest_time;
300 for (
const auto& entry :
301 std::filesystem::directory_iterator(states_dir, ec)) {
305 if (!entry.is_regular_file()) {
308 const auto ext = entry.path().extension().string();
309 if (ext !=
".state" && ext !=
".mss") {
312 const auto file_time = entry.last_write_time(ec);
316 if (!found || file_time > latest_time) {
318 latest_time = file_time;
319 latest_path = entry.path();
323 return ::absl::NotFoundError(::absl::StrFormat(
324 "no state files (.state/.mss) found in %s", states_dir.string()));
332 j[
"connected"] = s.connected;
333 j[
"running"] = s.running;
334 j[
"paused"] = s.paused;
335 j[
"frame"] = s.frame;
337 j[
"last_breakpoint_id"] = s.last_breakpoint_id;
338 j[
"last_action"] = s.last_action;
339 j[
"breakpoints"] = nlohmann::json::array();
340 for (
const auto& [
id, addr] : s.breakpoints_by_id) {
341 j[
"breakpoints"].push_back({{
"id",
id}, {
"address", addr}});
344 std::ofstream out(file_path);
345 if (!out.is_open()) {
346 return ::absl::PermissionDeniedError(
"Failed to open session export path");
350 return ::absl::InternalError(
"Failed to write session export file");
352 return ::absl::OkStatus();
356 std::ifstream in(file_path);
358 return ::absl::NotFoundError(
"Session import file not found");
363 }
catch (
const std::exception& e) {
364 return ::absl::InvalidArgumentError(
365 ::absl::StrFormat(
"Invalid session JSON: %s", e.what()));
369 loaded.
connected = j.value(
"connected",
false);
370 loaded.
running = j.value(
"running",
false);
371 loaded.
paused = j.value(
"paused",
false);
372 loaded.
frame = j.value(
"frame", 0ULL);
373 loaded.
pc = j.value(
"pc", 0U);
375 loaded.
last_action = j.value(
"last_action",
"import");
376 if (j.contains(
"breakpoints") && j[
"breakpoints"].is_array()) {
377 for (
const auto& bp : j[
"breakpoints"]) {
378 const int id = bp.value(
"id", -1);
379 const uint32_t addr = bp.value(
"address", 0U);
387 return ::absl::OkStatus();
392 if (!client->IsConnected()) {
393 std::string socket_path = ::absl::GetFlag(FLAGS_mesen_socket);
394 if (socket_path.empty()) {
395 const char* env_path = std::getenv(
"MESEN2_SOCKET_PATH");
396 if (env_path && env_path[0] !=
'\0') {
397 socket_path = env_path;
401 socket_path.empty() ? client->Connect() : client->Connect(socket_path);
403 return ::absl::UnavailableError(
404 "Not connected to Mesen2. Is Mesen2-OoS running?");
407 session.connected =
true;
408 session.last_action =
"connect";
411 return ::absl::OkStatus();
415 const std::string&
name,
418 return default_value;
425 hex.reserve(data_str.size());
426 for (
char c : data_str) {
427 if (std::isxdigit(
static_cast<unsigned char>(c))) {
432 std::vector<uint8_t> data;
433 for (
size_t i = 0; i + 1 < hex.size(); i += 2) {
435 auto res = std::from_chars(hex.data() + i, hex.data() + i + 2,
byte, 16);
436 if (res.ec == std::errc()) {
437 data.push_back(
static_cast<uint8_t
>(
byte));
444 const std::shared_ptr<emu::mesen::MesenSocketClient>& client,
445 ::absl::Time deadline) {
446 while (::absl::Now() < deadline) {
447 auto cpu_or = client->GetCpuState();
451 std::this_thread::sleep_for(std::chrono::milliseconds(5));
453 return ::absl::DeadlineExceededError(
454 "Timed out while polling Mesen2 CPU state");
463 return ::absl::OkStatus();
471 auto status = EnsureConnected();
476 auto result = client->GetGameState();
478 return result.status();
480 const auto& state = *result;
481 formatter.
AddField(
"link_x", state.link.x);
482 formatter.
AddField(
"link_y", state.link.y);
483 formatter.
AddField(
"link_layer", state.link.layer);
484 formatter.
AddField(
"link_direction",
static_cast<int>(state.link.direction));
485 formatter.
AddField(
"health",
static_cast<int>(state.items.current_health));
486 formatter.
AddField(
"max_health",
static_cast<int>(state.items.max_health));
487 formatter.
AddField(
"magic",
static_cast<int>(state.items.magic));
488 formatter.
AddField(
"rupees",
static_cast<int>(state.items.rupees));
489 formatter.
AddField(
"bombs",
static_cast<int>(state.items.bombs));
490 formatter.
AddField(
"arrows",
static_cast<int>(state.items.arrows));
491 formatter.
AddField(
"mode",
static_cast<int>(state.game.mode));
492 formatter.
AddField(
"submode",
static_cast<int>(state.game.submode));
493 formatter.
AddField(
"indoors", state.game.indoors);
494 if (state.game.indoors) {
495 formatter.
AddHexField(
"room_id", state.game.room_id, 4);
497 formatter.
AddHexField(
"overworld_area", state.game.overworld_area, 2);
500 return ::absl::OkStatus();
507 return ::absl::OkStatus();
514 auto status = EnsureConnected();
518 bool show_all = parser.
HasFlag(
"all");
520 auto result = client->GetSprites(show_all);
522 return result.status();
524 formatter.
AddField(
"count",
static_cast<int>(result->size()));
526 for (
const auto& sprite : *result) {
528 "[#%d] type=0x%02X state=%d @(%d,%d) hp=%d", sprite.slot, sprite.type,
529 sprite.state, sprite.x, sprite.y, sprite.health));
533 return ::absl::OkStatus();
540 return ::absl::OkStatus();
548 auto status = EnsureConnected();
553 auto result = client->GetCpuState();
555 return result.status();
557 const auto& cpu = *result;
558 uint32_t pc = (
static_cast<uint32_t
>(cpu.K) << 16) | (cpu.PC & 0xFFFF);
567 formatter.
AddField(
"emulation_mode", cpu.emulation_mode);
569 return ::absl::OkStatus();
582 auto status = EnsureConnected();
586 auto addr_or = parser.
GetHex(
"address");
588 return addr_or.status();
589 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
591 auto length_or = ParseOptionalInt(parser,
"length", 16);
593 return length_or.status();
594 int length = *length_or;
597 auto result = client->ReadBlock(addr, length);
599 return result.status();
602 formatter.
AddField(
"length",
static_cast<int>(result->size()));
604 for (uint8_t
byte : *result) {
605 formatter.
AddArrayItem(::absl::StrFormat(
"%02X",
byte));
609 return ::absl::OkStatus();
622 auto status = EnsureConnected();
626 auto addr_or = parser.
GetHex(
"address");
628 return addr_or.status();
629 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
631 auto data_str = parser.
GetString(
"data");
632 if (!data_str.has_value()) {
633 return ::absl::InvalidArgumentError(
"--data is required");
636 auto data = ParseHexBytes(*data_str);
638 return ::absl::InvalidArgumentError(
"Invalid --data hex string");
642 auto write_status = client->WriteBlock(addr, data);
643 if (!write_status.ok())
647 formatter.
AddField(
"bytes_written",
static_cast<int>(data.size()));
649 return ::absl::OkStatus();
662 auto status = EnsureConnected();
666 auto addr_or = parser.
GetHex(
"address");
668 return addr_or.status();
669 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
671 auto count_or = ParseOptionalInt(parser,
"count", 10);
673 return count_or.status();
674 int count = *count_or;
677 auto result = client->Disassemble(addr, count);
679 return result.status();
682 formatter.
AddField(
"disassembly", *result);
683 return ::absl::OkStatus();
690 return ::absl::OkStatus();
697 auto status = EnsureConnected();
701 auto count_or = ParseOptionalInt(parser,
"count", 20);
703 return count_or.status();
704 int count = *count_or;
707 auto result = client->GetTrace(count);
709 return result.status();
712 formatter.
AddField(
"trace", *result);
713 return ::absl::OkStatus();
726 auto status = EnsureConnected();
730 auto action = parser.
GetString(
"action");
731 if (!action.has_value()) {
732 return ::absl::InvalidArgumentError(
"--action is required");
735 if (*action ==
"add") {
736 auto addr_or = parser.
GetHex(
"address");
738 return addr_or.status();
739 uint32_t addr =
static_cast<uint32_t
>(*addr_or);
741 std::string type_str = parser.
GetString(
"type").value_or(
"exec");
743 if (type_str ==
"read")
745 else if (type_str ==
"write")
747 else if (type_str ==
"rw")
751 auto result = client->AddBreakpoint(addr, type);
753 return result.status();
754 formatter.
AddField(
"breakpoint_id", *result);
756 formatter.
AddField(
"status",
"added");
757 }
else if (*action ==
"remove") {
758 auto id_or = parser.
GetInt(
"id");
760 return id_or.status();
764 auto remove_status = client->RemoveBreakpoint(
id);
765 if (!remove_status.ok())
766 return remove_status;
767 formatter.
AddField(
"breakpoint_id",
id);
768 formatter.
AddField(
"status",
"removed");
769 }
else if (*action ==
"clear") {
771 auto clear_status = client->ClearBreakpoints();
772 if (!clear_status.ok())
774 formatter.
AddField(
"status",
"cleared");
775 }
else if (*action ==
"list") {
776 formatter.
AddField(
"status",
"not_implemented");
778 return ::absl::InvalidArgumentError(
"Unknown action: " + *action);
781 return ::absl::OkStatus();
794 auto status = EnsureConnected();
798 auto action = parser.
GetString(
"action");
799 if (!action.has_value()) {
800 return ::absl::InvalidArgumentError(
"--action required");
804 if (*action ==
"pause") {
805 auto result = client->Pause();
808 }
else if (*action ==
"resume") {
809 auto result = client->Resume();
812 }
else if (*action ==
"step") {
813 auto result = client->Step(1);
816 }
else if (*action ==
"frame") {
817 auto result = client->Frame();
820 }
else if (*action ==
"reset") {
821 auto result = client->Reset();
825 return ::absl::InvalidArgumentError(
"Unknown action: " + *action);
828 auto& session = SessionState();
829 session.last_action = *action;
830 UpdateSessionFromRuntime(client);
832 formatter.
AddField(
"action", *action);
833 return ::absl::OkStatus();
839 if (!parser.
GetString(
"action").has_value()) {
840 return ::absl::OkStatus();
842 const auto action = parser.
GetString(
"action").value_or(
"show");
843 if (action ==
"show" || action ==
"reset") {
844 return ::absl::OkStatus();
846 if (action ==
"export" || action ==
"import") {
849 return ::absl::InvalidArgumentError(
850 "--action must be show|reset|export|import");
854 const auto& session = SessionState();
855 formatter.
AddField(
"connected", session.connected);
856 formatter.
AddField(
"running", session.running);
857 formatter.
AddField(
"paused", session.paused);
858 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
860 formatter.
AddField(
"breakpoint_count",
861 static_cast<int>(session.breakpoints_by_id.size()));
862 formatter.
AddField(
"last_breakpoint_id", session.last_breakpoint_id);
863 formatter.
AddField(
"last_action", session.last_action);
864 return ::absl::OkStatus();
871 const auto action = parser.
GetString(
"action").value_or(
"show");
872 if (action ==
"reset") {
874 auto& session = SessionState();
875 session.last_action =
"reset";
877 formatter.
AddField(
"action",
"reset");
881 if (action ==
"export") {
883 if (!file.has_value()) {
884 return ::absl::InvalidArgumentError(
"--file required for export");
886 auto export_status = ExportSessionStateToFile(*file);
887 if (!export_status.ok()) {
888 return export_status;
890 auto& session = SessionState();
891 session.last_action =
"export";
893 formatter.
AddField(
"action",
"export");
898 if (action ==
"import") {
900 if (!file.has_value()) {
901 return ::absl::InvalidArgumentError(
"--file required for import");
903 auto import_status = ImportSessionStateFromFile(*file);
904 if (!import_status.ok()) {
905 return import_status;
907 auto& session = SessionState();
908 session.last_action =
"import";
910 formatter.
AddField(
"action",
"import");
915 auto status = EnsureConnected();
920 UpdateSessionFromRuntime(client);
931 const auto type = parser.
GetString(
"type").value_or(
"");
932 if (type ==
"frame") {
938 if (type ==
"breakpoint") {
941 return ::absl::InvalidArgumentError(
942 "--type must be one of frame|pc|breakpoint");
949 auto status = EnsureConnected();
954 auto& session = SessionState();
956 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
957 if (!timeout_or.ok()) {
958 return timeout_or.status();
960 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
962 return poll_or.status();
964 const int timeout_ms = ClampTimeoutMs(*timeout_or);
965 const int poll_ms = ClampPollMs(*poll_or);
966 const auto start = ::absl::Now();
967 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
969 const auto type = parser.
GetString(
"type").value_or(
"");
970 if (type ==
"frame") {
971 auto count_or = parser.
GetInt(
"count");
972 if (!count_or.ok()) {
973 return count_or.status();
975 UpdateSessionFromRuntime(client);
976 const uint64_t target =
977 session.frame +
static_cast<uint64_t
>(std::max(0, *count_or));
978 while (::absl::Now() < deadline) {
979 if (
auto state_or = client->GetState(); state_or.ok()) {
980 session.frame = state_or->frame;
981 session.running = state_or->running;
982 session.paused = state_or->paused;
983 if (session.frame >= target) {
984 session.last_action =
"await-frame";
986 formatter.
AddField(
"type",
"frame");
987 formatter.
AddField(
"target_frame",
static_cast<uint64_t
>(target));
988 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
990 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
991 ::absl::Now() - start)));
992 return ::absl::OkStatus();
995 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
997 return ::absl::DeadlineExceededError(
"Timed out waiting for target frame");
1001 auto addr_or = parser.
GetHex(
"address");
1002 if (!addr_or.ok()) {
1003 return addr_or.status();
1005 const uint32_t target_pc =
static_cast<uint32_t
>(*addr_or);
1006 while (::absl::Now() < deadline) {
1007 auto cpu_or = PollCpuStateWithDeadline(client, deadline);
1009 return cpu_or.status();
1012 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1014 if (pc == target_pc) {
1015 session.last_action =
"await-pc";
1016 formatter.
AddField(
"status",
"ok");
1021 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
1022 ::absl::Now() - start)));
1023 return ::absl::OkStatus();
1025 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1027 return ::absl::DeadlineExceededError(
"Timed out waiting for target pc");
1030 auto id_or = parser.
GetInt(
"id");
1032 return id_or.status();
1034 const int id = *id_or;
1035 const auto it = session.breakpoints_by_id.find(
id);
1036 if (it == session.breakpoints_by_id.end()) {
1037 return ::absl::NotFoundError(
"Unknown breakpoint id in session state");
1039 const uint32_t target_pc = it->second;
1040 while (::absl::Now() < deadline) {
1041 auto state_or = client->GetState();
1042 if (!state_or.ok()) {
1043 return state_or.status();
1045 session.running = state_or->running;
1046 session.paused = state_or->paused;
1047 session.frame = state_or->frame;
1048 auto cpu_or = client->GetCpuState();
1050 return cpu_or.status();
1053 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1055 if (session.paused && pc == target_pc) {
1056 session.last_action =
"await-breakpoint";
1057 formatter.
AddField(
"status",
"ok");
1058 formatter.
AddField(
"type",
"breakpoint");
1059 formatter.
AddField(
"breakpoint_id",
id);
1061 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1063 static_cast<uint64_t
>(::absl::ToInt64Milliseconds(
1064 ::absl::Now() - start)));
1065 return ::absl::OkStatus();
1067 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1069 return ::absl::DeadlineExceededError(
"Timed out waiting for breakpoint hit");
1079 const auto goal = parser.
GetString(
"goal").value_or(
"");
1080 if (goal ==
"break-at" || goal ==
"capture-state-at-pc") {
1083 if (goal ==
"run-frames") {
1086 return ::absl::InvalidArgumentError(
1087 "Unknown --goal. Supported: break-at|run-frames|capture-state-at-pc");
1094 auto status = EnsureConnected();
1099 auto& session = SessionState();
1100 const auto goal = parser.
GetString(
"goal").value_or(
"");
1101 if (goal ==
"run-frames") {
1102 auto count_or = parser.
GetInt(
"count");
1103 if (!count_or.ok()) {
1104 return count_or.status();
1106 const int frame_count = std::max(1, *count_or);
1107 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
1108 if (!timeout_or.ok()) {
1109 return timeout_or.status();
1111 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
1112 if (!poll_or.ok()) {
1113 return poll_or.status();
1115 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1116 const int poll_ms = ClampPollMs(*poll_or);
1117 const auto start = ::absl::Now();
1118 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1120 if (
auto pause_status = client->Pause(); !pause_status.ok()) {
1121 return pause_status;
1123 UpdateSessionFromRuntime(client);
1124 const uint64_t target_frame =
1125 session.frame +
static_cast<uint64_t
>(frame_count);
1126 if (
auto resume_status = client->Resume(); !resume_status.ok()) {
1127 return resume_status;
1129 bool reached =
false;
1130 while (::absl::Now() < deadline) {
1131 auto state_or = client->GetState();
1132 if (!state_or.ok()) {
1135 session.running = state_or->running;
1136 session.paused = state_or->paused;
1137 session.frame = state_or->frame;
1138 if (session.frame >= target_frame) {
1142 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1144 (void)client->Pause();
1145 UpdateSessionFromRuntime(client);
1147 return ::absl::DeadlineExceededError(
"Goal run-frames timed out");
1149 session.last_action =
"goal-run-frames";
1150 formatter.
AddField(
"status",
"ok");
1151 formatter.
AddField(
"goal",
"run-frames");
1152 formatter.
AddField(
"requested_frames", frame_count);
1153 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1155 static_cast<uint64_t
>(
1156 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1157 return ::absl::OkStatus();
1160 if (goal !=
"break-at" && goal !=
"capture-state-at-pc") {
1161 return ::absl::InvalidArgumentError(
"Unsupported goal");
1163 auto addr_or = parser.
GetHex(
"address");
1164 if (!addr_or.ok()) {
1165 return addr_or.status();
1167 const uint32_t target_pc =
static_cast<uint32_t
>(*addr_or);
1168 auto timeout_or = ParseOptionalInt(parser,
"timeout-ms", 2000);
1169 if (!timeout_or.ok()) {
1170 return timeout_or.status();
1172 auto poll_or = ParseOptionalInt(parser,
"poll-ms", 25);
1173 if (!poll_or.ok()) {
1174 return poll_or.status();
1176 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1177 const int poll_ms = ClampPollMs(*poll_or);
1178 const auto start = ::absl::Now();
1179 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1181 if (
auto pause_status = client->Pause(); !pause_status.ok()) {
1182 return pause_status;
1187 return bp_or.status();
1189 const int bp_id = *bp_or;
1190 session.breakpoints_by_id[bp_id] = target_pc;
1191 session.last_breakpoint_id = bp_id;
1193 if (
auto resume_status = client->Resume(); !resume_status.ok()) {
1194 (void)client->RemoveBreakpoint(bp_id);
1195 session.breakpoints_by_id.erase(bp_id);
1196 return resume_status;
1200 while (::absl::Now() < deadline) {
1201 auto state_or = client->GetState();
1202 if (!state_or.ok()) {
1205 session.running = state_or->running;
1206 session.paused = state_or->paused;
1207 session.frame = state_or->frame;
1208 auto cpu_or = client->GetCpuState();
1213 (
static_cast<uint32_t
>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1215 if (session.paused && pc == target_pc) {
1219 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1222 (void)client->Pause();
1223 (void)client->RemoveBreakpoint(bp_id);
1224 session.breakpoints_by_id.erase(bp_id);
1225 UpdateSessionFromRuntime(client);
1227 return ::absl::DeadlineExceededError(
"Goal break-at timed out");
1230 session.last_action = (goal ==
"capture-state-at-pc")
1231 ?
"goal-capture-state-at-pc"
1233 if (goal ==
"capture-state-at-pc") {
1234 auto game_state_or = client->GetGameState();
1235 if (game_state_or.ok()) {
1236 formatter.
AddHexField(
"room_id", game_state_or->game.room_id, 4);
1237 formatter.
AddField(
"indoors", game_state_or->game.indoors);
1238 formatter.
AddField(
"link_x", game_state_or->link.x);
1239 formatter.
AddField(
"link_y", game_state_or->link.y);
1240 formatter.
AddField(
"health", game_state_or->items.current_health);
1243 formatter.
AddField(
"status",
"ok");
1245 formatter.
AddField(
"breakpoint_id", bp_id);
1248 formatter.
AddField(
"frame",
static_cast<uint64_t
>(session.frame));
1250 static_cast<uint64_t
>(
1251 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1252 return ::absl::OkStatus();
1265 const std::string state_path = *parser.
GetString(
"state");
1266 const std::string rom_path = *parser.
GetString(
"rom-file");
1267 const std::string meta_path =
1268 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1269 const std::string expected_scenario = OptionalScenario(parser);
1271 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1273 if (!result_or.ok()) {
1274 return result_or.status();
1276 AddSavestateFreshnessFields(formatter, *result_or);
1277 if (!result_or->fresh) {
1278 return ::absl::FailedPreconditionError(
1279 "Savestate freshness verification failed");
1281 return ::absl::OkStatus();
1294 const std::string state_path = *parser.
GetString(
"state");
1295 const std::string rom_path = *parser.
GetString(
"rom-file");
1296 const std::string meta_path =
1297 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1298 const std::string scenario = OptionalScenario(parser);
1300 auto meta_or = BuildSavestateMetadata(state_path, rom_path, scenario,
1301 "z3ed mesen-state-regen");
1302 if (!meta_or.ok()) {
1303 return meta_or.status();
1306 auto write_status = WriteJsonFile(meta_path, *meta_or);
1307 if (!write_status.ok())
1308 return write_status;
1310 formatter.
AddField(
"status",
"ok");
1311 formatter.
AddField(
"state", state_path);
1312 formatter.
AddField(
"rom", rom_path);
1313 formatter.
AddField(
"meta", meta_path);
1314 formatter.
AddField(
"rom_sha1", meta_or->value(
"rom_sha1",
""));
1315 formatter.
AddField(
"state_sha1", meta_or->value(
"state_sha1",
""));
1316 formatter.
AddField(
"scenario", scenario);
1317 return ::absl::OkStatus();
1330 const std::string requested_state_path = *parser.
GetString(
"state");
1331 const std::string rom_path = *parser.
GetString(
"rom-file");
1332 const std::string scenario = OptionalScenario(parser);
1333 const std::string meta_path = parser.
GetString(
"meta").value_or(
1334 DefaultMetaPathForState(requested_state_path));
1335 const int wait_ms = parser.
GetInt(
"wait-ms").value_or(400);
1336 const int slot = parser.
GetInt(
"slot").value_or(-1);
1337 const std::string states_dir = parser.
GetString(
"states-dir").value_or(
"");
1339 auto rom_status = EnsureFileExists(rom_path,
"ROM file");
1340 if (!rom_status.ok()) {
1344 std::string resolved_state_path = requested_state_path;
1345 bool captured_from_mesen_slot =
false;
1347 auto status = EnsureConnected();
1352 auto save_status = client->SaveState(slot);
1353 if (!save_status.ok()) {
1356 captured_from_mesen_slot =
true;
1357 std::this_thread::sleep_for(
1358 std::chrono::milliseconds(std::max(1, wait_ms)));
1362 if (!std::filesystem::exists(resolved_state_path, ec) || ec) {
1363 if (states_dir.empty()) {
1364 return ::absl::NotFoundError(
1365 ::absl::StrFormat(
"state file not found after capture: %s (use "
1366 "--states-dir to auto-locate latest state file)",
1367 resolved_state_path));
1369 auto latest_or = FindLatestStateFileInDir(states_dir);
1370 if (!latest_or.ok()) {
1371 return latest_or.status();
1373 const auto source_path = *latest_or;
1374 auto parent = std::filesystem::path(resolved_state_path).parent_path();
1375 if (!parent.empty()) {
1376 std::filesystem::create_directories(parent, ec);
1378 return ::absl::PermissionDeniedError(::absl::StrFormat(
1379 "failed to create directory: %s", parent.string()));
1382 std::filesystem::copy_file(
1383 source_path, resolved_state_path,
1384 std::filesystem::copy_options::overwrite_existing, ec);
1386 return ::absl::InternalError(::absl::StrFormat(
1387 "failed to copy state from %s to %s: %s", source_path.string(),
1388 resolved_state_path, ec.message()));
1392 auto meta_or = BuildSavestateMetadata(resolved_state_path, rom_path, scenario,
1393 "z3ed mesen-state-capture");
1394 if (!meta_or.ok()) {
1395 return meta_or.status();
1397 auto write_status = WriteJsonFile(meta_path, *meta_or);
1398 if (!write_status.ok()) {
1399 return write_status;
1402 auto verify_or = ComputeSavestateFreshness(resolved_state_path, rom_path,
1403 meta_path, scenario);
1404 if (!verify_or.ok()) {
1405 return verify_or.status();
1407 formatter.
AddField(
"status",
"ok");
1408 formatter.
AddField(
"state", resolved_state_path);
1409 formatter.
AddField(
"rom", rom_path);
1410 formatter.
AddField(
"meta", meta_path);
1411 formatter.
AddField(
"scenario", scenario);
1412 formatter.
AddField(
"captured_from_mesen_slot", captured_from_mesen_slot);
1414 formatter.
AddField(
"fresh", verify_or->fresh);
1415 formatter.
AddField(
"rom_sha1", meta_or->value(
"rom_sha1",
""));
1416 formatter.
AddField(
"state_sha1", meta_or->value(
"state_sha1",
""));
1417 return ::absl::OkStatus();
1430 const std::string state_path = *parser.
GetString(
"state");
1431 const std::string rom_path = *parser.
GetString(
"rom-file");
1432 const std::string meta_path =
1433 parser.
GetString(
"meta").value_or(DefaultMetaPathForState(state_path));
1434 const std::string expected_scenario = OptionalScenario(parser);
1436 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1438 if (!result_or.ok()) {
1439 return result_or.status();
1442 AddSavestateFreshnessFields(formatter, *result_or);
1446 "fresh=%s scenario=%s rom_sha1=%s state_sha1=%s",
1447 result_or->fresh ?
"true" :
"false",
1448 result_or->recorded_scenario.empty() ?
"(none)"
1449 : result_or->recorded_scenario,
1450 result_or->current_rom_sha1, result_or->current_state_sha1));
1451 if (!result_or->fresh) {
1452 return ::absl::FailedPreconditionError(
"mesen preflight hook failed");
1454 return ::absl::OkStatus();
1458std::vector<std::unique_ptr<resources::CommandHandler>>
1460 std::vector<std::unique_ptr<resources::CommandHandler>> handlers;
1461 handlers.push_back(std::make_unique<MesenGamestateCommandHandler>());
1462 handlers.push_back(std::make_unique<MesenSpritesCommandHandler>());
1463 handlers.push_back(std::make_unique<MesenCpuCommandHandler>());
1464 handlers.push_back(std::make_unique<MesenMemoryReadCommandHandler>());
1465 handlers.push_back(std::make_unique<MesenMemoryWriteCommandHandler>());
1466 handlers.push_back(std::make_unique<MesenDisasmCommandHandler>());
1467 handlers.push_back(std::make_unique<MesenTraceCommandHandler>());
1468 handlers.push_back(std::make_unique<MesenBreakpointCommandHandler>());
1469 handlers.push_back(std::make_unique<MesenControlCommandHandler>());
1470 handlers.push_back(std::make_unique<MesenSessionCommandHandler>());
1471 handlers.push_back(std::make_unique<MesenAwaitCommandHandler>());
1472 handlers.push_back(std::make_unique<MesenGoalCommandHandler>());
1473 handlers.push_back(std::make_unique<MesenStateVerifyCommandHandler>());
1474 handlers.push_back(std::make_unique<MesenStateRegenCommandHandler>());
1475 handlers.push_back(std::make_unique<MesenStateCaptureCommandHandler>());
1476 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< nlohmann::json > LoadJsonFile(const std::string &path)
::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